diff --git a/src/main/java/org/mtransit/android/commons/data/Schedule.java b/src/main/java/org/mtransit/android/commons/data/Schedule.java index a21b1279..1b31596d 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -27,7 +27,7 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; -@SuppressWarnings({"unused", "WeakerAccess"}) +@SuppressWarnings("WeakerAccess") public class Schedule extends POIStatus implements MTLog.Loggable { private static final String LOG_TAG = Schedule.class.getSimpleName(); @@ -189,6 +189,7 @@ public void setNoPickup(boolean noPickup) { this.noPickup = noPickup; } + @SuppressWarnings("unused") public void setNoPickupTimestamps(boolean noPickup) { for (Timestamp timestamp : this.timestamps) { if (noPickup) { @@ -318,6 +319,7 @@ public int compare(Frequency lhs, Frequency rhs) { } } + @SuppressWarnings("unused") public static class Frequency implements MTLog.Loggable { private static final String LOG_TAG = Frequency.class.getSimpleName(); @@ -452,6 +454,8 @@ public String getLogTag() { @Nullable private Integer accessible = null; @Nullable + private Boolean cancelled = null; + @Nullable private String tripId = null; // cleaned trip ID (string) // initial used to store trip id INT but replaced after private int stopSequence = -1; @Nullable @@ -550,6 +554,7 @@ public boolean hasHeadsign() { return this.headsignType != Direction.HEADSIGN_TYPE_NONE && !TextUtils.isEmpty(this.headsignValue); } + @SuppressWarnings("unused") // main app @NonNull public String getUIHeading(@NonNull Context context, boolean small) { final String headSignUC = getHeading(context); @@ -620,15 +625,12 @@ public void setRealTime(@Nullable Boolean realTime) { this.realTime = realTime; } + @SuppressWarnings("unused") // kotlin var @Nullable public Boolean getRealTime() { return this.realTime; } - boolean hasRealTime() { - return this.realTime != null; - } - public boolean isRealTime() { return Boolean.TRUE.equals(this.realTime); } @@ -637,15 +639,12 @@ public void setOldSchedule(@Nullable Boolean oldSchedule) { this.oldSchedule = oldSchedule; } + @SuppressWarnings("unused") // kotlin var @Nullable public Boolean getOldSchedule() { return this.oldSchedule; } - boolean hasOldSchedule() { - return this.oldSchedule != null; - } - public boolean isOldSchedule() { return Boolean.TRUE.equals(this.oldSchedule); } @@ -659,14 +658,24 @@ public Integer getAccessible() { return accessible; } - public boolean hasAccessible() { - return this.accessible != null; - } - + @SuppressWarnings("unused") // main app public int getAccessibleOrDefault() { return this.accessible == null ? Accessibility.DEFAULT : this.accessible; } + public void setCancelled(@Nullable Boolean cancelled) { + this.cancelled = cancelled; + } + + @Nullable + public Boolean getCancelled() { + return cancelled; + } + + public boolean isCancelled() { + return Boolean.TRUE.equals(this.cancelled); + } + public void setTripId(@Nullable String tripId) { this.tripId = tripId; } @@ -701,6 +710,7 @@ public boolean equals(Object o) { if (!Objects.equals(realTime, timestamp.realTime)) return false; if (!Objects.equals(oldSchedule, timestamp.oldSchedule)) return false; if (!Objects.equals(accessible, timestamp.accessible)) return false; + if (!Objects.equals(cancelled, timestamp.cancelled)) return false; if (!Objects.equals(tripId, timestamp.tripId)) return false; if (stopSequence != timestamp.stopSequence) return false; if (!Objects.equals(arrivalDiffMs, timestamp.arrivalDiffMs)) return false; @@ -719,6 +729,7 @@ public int hashCode() { result = 31 * result + (realTime != null ? realTime.hashCode() : 0); result = 31 * result + (oldSchedule != null ? oldSchedule.hashCode() : 0); result = 31 * result + (accessible != null ? accessible : 0); + result = 31 * result + (cancelled != null ? cancelled.hashCode() : 0); result = 31 * result + (tripId != null ? tripId.hashCode() : 0); result = 31 * result + stopSequence; result = 31 * result + (arrivalDiffMs != null ? arrivalDiffMs.hashCode() : 0); @@ -766,6 +777,9 @@ public String toString() { if (accessible != null) { sb.append(", a11y:").append(accessible); } + if (cancelled != null) { + sb.append(", cancelled:").append(cancelled); + } sb.append('}'); return sb.toString(); } @@ -782,6 +796,7 @@ public String toString() { private static final String JSON_REAL_TIME = "rt"; private static final String JSON_OLD_SCHEDULE = "old"; private static final String JSON_ACCESSIBLE = "a11y"; + private static final String JSON_CANCELLED = "cancelled"; @Nullable static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { @@ -827,6 +842,9 @@ static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { if (jTimestamp.has(JSON_ACCESSIBLE)) { timestamp.setAccessible(jTimestamp.optInt(JSON_ACCESSIBLE, Accessibility.DEFAULT)); } + if (jTimestamp.has(JSON_CANCELLED)) { + timestamp.setCancelled(jTimestamp.optBoolean(JSON_CANCELLED, false)); + } return timestamp; } catch (JSONException jsone) { MTLog.w(LOG_TAG, jsone, "Error while parsing JSON object '%s'!", jTimestamp); @@ -870,15 +888,18 @@ public static JSONObject toJSON(@NonNull Timestamp timestamp) { if (timestamp.localTimeZoneId != null) { jTimestamp.put(JSON_LOCAL_TIME_ZONE, timestamp.localTimeZoneId); } - if (timestamp.hasRealTime()) { + if (timestamp.realTime != null) { jTimestamp.put(JSON_REAL_TIME, timestamp.realTime); } - if (timestamp.hasOldSchedule()) { + if (timestamp.oldSchedule != null) { jTimestamp.put(JSON_OLD_SCHEDULE, timestamp.oldSchedule); } - if (timestamp.hasAccessible()) { + if (timestamp.accessible != null) { jTimestamp.put(JSON_ACCESSIBLE, timestamp.accessible); } + if (timestamp.cancelled != null) { + jTimestamp.put(JSON_CANCELLED, timestamp.cancelled); + } return jTimestamp; } catch (Exception e) { MTLog.w(LOG_TAG, e, "Error while converting object '%s' to JSON!", timestamp); @@ -897,6 +918,7 @@ public String getLogTag() { return LOG_TAG; } + @SuppressWarnings("unused") // main app public static final int DATA_REQUEST_MONTHS = 62; @SuppressWarnings("unused") public static final int DATA_REQUEST_YEAR = 365; @@ -916,10 +938,8 @@ public String getLogTag() { private Integer minUsefulResults = null; @Nullable private Integer maxDataRequests = null; - - public ScheduleStatusFilter(@NonNull String targetUUID, @NonNull RouteDirectionStop rds) { - this(rds); - } + @Nullable + private Boolean includeCancelledTimestamps = null; public ScheduleStatusFilter(@NonNull RouteDirectionStop rds) { super(POI.ITEM_STATUS_TYPE_SCHEDULE, rds.getUUID()); @@ -982,6 +1002,15 @@ public void setMaxDataRequests(@Nullable Integer maxDataRequests) { this.maxDataRequests = maxDataRequests; } + public boolean isIncludeCancelledTimestampsOrDefault() { + return Boolean.TRUE.equals(this.includeCancelledTimestamps); + } + + @SuppressWarnings("unused") // main app + public void setIncludeCancelledTimestamps(@Nullable Boolean includeCancelledTimestamps) { + this.includeCancelledTimestamps = includeCancelledTimestamps; + } + private static long getNewDefaultTimestamp() { return TimeUtils.currentTimeToTheMinuteMillis(); } @@ -1007,22 +1036,24 @@ public static StatusProviderContract.Filter fromJSONString(@Nullable String json private static final String JSON_MAX_DATA_REQUESTS = "maxDataRequests"; private static final String JSON_ROUTE_DIRECTION_STOP = "routeTripStop"; // do not change to avoid breaking compat w/ old modules private static final String JSON_LOOK_BEHIND_IN_MS = "lookBehindInMs"; + private static final String JSON_INCLUDE_CANCELLED_TIMESTAMPS = "includeCancelledTimestamps"; @Nullable public static StatusProviderContract.Filter fromJSON(@NonNull JSONObject json) { try { - String targetUUID = StatusProviderContract.Filter.getTargetUUIDFromJSON(json); - RouteDirectionStop routeDirectionStop = RouteDirectionStop.fromJSONStatic(json.getJSONObject(JSON_ROUTE_DIRECTION_STOP)); + final RouteDirectionStop routeDirectionStop = RouteDirectionStop.fromJSONStatic(json.getJSONObject(JSON_ROUTE_DIRECTION_STOP)); if (routeDirectionStop == null) { return null; } - ScheduleStatusFilter scheduleStatusFilter = new ScheduleStatusFilter(targetUUID, routeDirectionStop); + final ScheduleStatusFilter scheduleStatusFilter = new ScheduleStatusFilter(routeDirectionStop); StatusProviderContract.Filter.fromJSON(scheduleStatusFilter, json); scheduleStatusFilter.lookBehindInMs = json.has(JSON_LOOK_BEHIND_IN_MS) ? json.getLong(JSON_LOOK_BEHIND_IN_MS) : null; scheduleStatusFilter.minUsefulDurationCoveredInMs = json.has(JSON_MIN_USEFUL_DURATION_COVERED_IN_MS) ? json.getLong(JSON_MIN_USEFUL_DURATION_COVERED_IN_MS) : null; scheduleStatusFilter.minUsefulResults = json.has(JSON_MIN_USEFUL_RESULTS) ? json.getInt(JSON_MIN_USEFUL_RESULTS) : null; scheduleStatusFilter.maxDataRequests = json.has(JSON_MAX_DATA_REQUESTS) ? json.getInt(JSON_MAX_DATA_REQUESTS) : null; + scheduleStatusFilter.includeCancelledTimestamps = + json.has(JSON_INCLUDE_CANCELLED_TIMESTAMPS) ? json.optBoolean(JSON_INCLUDE_CANCELLED_TIMESTAMPS, false) : null; return scheduleStatusFilter; } catch (JSONException jsone) { MTLog.w(LOG_TAG, jsone, "Error while parsing JSON object '%s'", json); @@ -1038,17 +1069,17 @@ public String toJSONStringStatic(@NonNull StatusProviderContract.Filter statusFi @Nullable static String toJSONString(@NonNull StatusProviderContract.Filter statusFilter) { - JSONObject json = toJSON(statusFilter); + final JSONObject json = toJSON(statusFilter); return json == null ? null : json.toString(); } @Nullable public static JSONObject toJSON(@NonNull StatusProviderContract.Filter statusFilter) { try { - JSONObject json = new JSONObject(); + final JSONObject json = new JSONObject(); StatusProviderContract.Filter.toJSON(statusFilter, json); if (statusFilter instanceof ScheduleStatusFilter) { - ScheduleStatusFilter scheduleFilter = (ScheduleStatusFilter) statusFilter; + final ScheduleStatusFilter scheduleFilter = (ScheduleStatusFilter) statusFilter; json.put(JSON_ROUTE_DIRECTION_STOP, scheduleFilter.routeDirectionStop.toJSON()); if (scheduleFilter.lookBehindInMs != null) { json.put(JSON_LOOK_BEHIND_IN_MS, scheduleFilter.lookBehindInMs); @@ -1062,6 +1093,9 @@ public static JSONObject toJSON(@NonNull StatusProviderContract.Filter statusFil if (scheduleFilter.maxDataRequests != null) { json.put(JSON_MAX_DATA_REQUESTS, scheduleFilter.maxDataRequests); } + if (scheduleFilter.includeCancelledTimestamps != null) { + json.put(JSON_INCLUDE_CANCELLED_TIMESTAMPS, scheduleFilter.includeCancelledTimestamps); + } } return json; } catch (JSONException jsone) { @@ -1080,6 +1114,7 @@ public String toString() { ", minUsefulDurationCoveredInMs=" + minUsefulDurationCoveredInMs + ", minUsefulResults=" + minUsefulResults + ", maxDataRequests=" + maxDataRequests + + ", includeCancelledTimestamps=" + includeCancelledTimestamps + '}'; } } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsStatusProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsStatusProviderExt.kt index ace35e7e..4e92f2de 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsStatusProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsStatusProviderExt.kt @@ -12,13 +12,15 @@ import kotlin.time.Duration.Companion.hours fun Context.getRDSSchedule( authority: String, rdsList: Iterable, + includeCancelledTimestamps: Boolean = false, ) = rdsList.mapNotNull { - getRDSSchedule(authority, it) + getRDSSchedule(authority, it, includeCancelledTimestamps) } fun Context.getRDSSchedule( authority: String, rds: RouteDirectionStop, + includeCancelledTimestamps: Boolean = false, ): Schedule? = try { contentResolver.query( Uri.withAppendedPath( @@ -29,6 +31,7 @@ fun Context.getRDSSchedule( Schedule.ScheduleStatusFilter(rds).apply { setLookBehindInMs(1.hours.inWholeMilliseconds) setMaxDataRequests(3) // yesterday service ending + today + tomorrow? + setIncludeCancelledTimestamps(includeCancelledTimestamps) }.let { it.toJSONStringStatic(it) }, null, null diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt index bd094f93..2db1233e 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt @@ -155,13 +155,13 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { uuidSchedule = sortedRDS ?.let { rdsList -> context - .getRDSSchedule(targetAuthority, rdsList) + .getRDSSchedule(targetAuthority, rdsList, filter.isIncludeCancelledTimestampsOrDefault) .associateBy { it.targetUUID } } } uuidSchedule ?: return null sortedRDS ?: return null - processRDTripUpdates(rdTripUpdates, uuidSchedule, sortedRDS) + processRDTripUpdates(rdTripUpdates, uuidSchedule, sortedRDS, filter.isIncludeCancelledTimestampsOrDefault) val tripsWithRealTime = uuidSchedule.values .asSequence() .mapNotNull { it?.timestamps }.flatten() diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderExt.kt index 477ed6a7..6581e0c7 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderExt.kt @@ -40,7 +40,8 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.Schedu fun GTFSRealTimeProvider.processRDTripUpdates( rdTripUpdates: List>, targetUuidSchedule: Map, - sortedRDS: List + sortedRDS: List, + includeCancelledTimestamps: Boolean = false, ) { rdTripUpdates.forEach { (td, gTripUpdate) -> val gTripId = td.optTripId ?: return@forEach @@ -57,6 +58,7 @@ fun GTFSRealTimeProvider.processRDTripUpdates( processRDTripUpdate( tripId, gTripUpdate, tripSortedRDS, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop = { stu, rds, stopSeq -> isSameStop(stu, rds, stopSeq) }, + includeCancelledTimestamps = includeCancelledTimestamps, ) } } @@ -95,10 +97,9 @@ internal fun processRDTripUpdate( sortedTargetUuidAndSequence: List>, tripTargetUuidSchedule: Map, isSameStop: (GTUStopTimeUpdate?, RouteDirectionStop, Int) -> Boolean, + includeCancelledTimestamps: Boolean = false, ) { - if (gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.CANCELED - || gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.DELETED - ) { + if (gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.DELETED) { tripTargetUuidSchedule.values.forEach { schedule -> schedule ?: return@forEach schedule.timestamps.filter { it.tripId == tripId }.forEach { @@ -107,6 +108,19 @@ internal fun processRDTripUpdate( } return } + if (gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.CANCELED) { + tripTargetUuidSchedule.values.forEach { schedule -> + schedule ?: return@forEach + schedule.timestamps.filter { it.tripId == tripId }.forEach { timestamp -> + if (includeCancelledTimestamps) { + timestamp.cancelled = true + } else { + schedule.removeTimestamp(timestamp) + } + } + } + return + } if (gTripUpdate.optDelay == null && gTripUpdate.stopTimeUpdateCount == 0) { MTLog.d(LOG_TAG, "processRDTripUpdate($tripId) > SKIP (useless trip update: ${gTripUpdate.toStringExt()})") return // nothing to do @@ -125,14 +139,26 @@ internal fun processRDTripUpdate( while (!isSameStop(nextStopTimeUpdate, currentRDS, currentUuidAndSeq.second) && uuidAndSeqIdx <= sortedTargetUuidAndSequence.size // allow null currentRDS to signify end of trip ) { - currentDelay = applyDelay(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentDelay) + currentDelay = applyDelay( + tripId = tripId, + stopSequence = currentUuidAndSeq.second, + rdsSchedule = tripTargetUuidSchedule[currentRDS.uuid], + currentDelay = currentDelay, + ) currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } if (uuidAndSeqIdx >= sortedTargetUuidAndSequence.size) break // no more stop currentStopTimeUpdate = nextStopTimeUpdate ?: break // no more stop time update nextStopTimeUpdate = gStopTimeUpdates?.getOrNull(++stuIdx) - currentDelay = applyDelaySTU(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) + currentDelay = applyDelaySTU( + tripId = tripId, + stopSequence = currentUuidAndSeq.second, + rdsSchedule = tripTargetUuidSchedule[currentRDS.uuid], + gStopTimeUpdate = currentStopTimeUpdate, + currentDelay = currentDelay, + includeCancelledTimestamps = includeCancelledTimestamps, + ) currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } @@ -157,6 +183,7 @@ internal fun applyDelaySTU( rdsSchedule: Schedule?, gStopTimeUpdate: GTUStopTimeUpdate, currentDelay: Duration? = null, + includeCancelledTimestamps: Boolean = false, ): Duration? { val rdsTripTimestamp = rdsSchedule?.timestamps?.findClosestTripTimestamp(tripId, stopSequence) ?: return null // impossible to handle @@ -178,11 +205,15 @@ internal fun applyDelaySTU( .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .makeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) stuArrivalTime?.let { rdsTripTimestamp.updateArrivalForRealTime(newArrival = it) } - ?: stuArrivalDelay?.let { rdsTripTimestamp.updateArrivalForRealTime(arrivalDelay = it, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) } + ?: stuArrivalDelay?.let { rdsTripTimestamp.updateArrivalForRealTime(it, rdsSchedule.providerPrecision, PROVIDER_PRECISION) } stuDepartureTime?.let { rdsTripTimestamp.updateDepartureForRealTime(newDeparture = it) } - ?: stuDepartureDelay?.let { rdsTripTimestamp.updateDepartureForRealTime(departureDelay = it, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) } + ?: stuDepartureDelay?.let { rdsTripTimestamp.updateDepartureForRealTime(it, rdsSchedule.providerPrecision, PROVIDER_PRECISION) } if (gStopTimeUpdate.scheduleRelationship == GTUSTUScheduleRelationship.SKIPPED) { - rdsSchedule.removeTimestamp(rdsTripTimestamp) + if (includeCancelledTimestamps) { + rdsTripTimestamp.cancelled = true + } else { + rdsSchedule.removeTimestamp(rdsTripTimestamp) + } } return stuDepartureDelay .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } @@ -213,14 +244,14 @@ internal fun applyDelay( ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.arrivalDiff if (currentDelay < Duration.ZERO) { - rdsTripTimestamp.updateForRealTime(delay = currentDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) + rdsTripTimestamp.updateForRealTime(delay = currentDelay, rdsSchedule.providerPrecision, PROVIDER_PRECISION) return currentDelay // do not consume negative delay } else if (currentDiffBetweenArrivalAndDeparture <= currentDelay) { val newDelay = (currentDelay - currentDiffBetweenArrivalAndDeparture).coerceAtLeast(Duration.ZERO) - rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = newDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) + rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = newDelay, rdsSchedule.providerPrecision, PROVIDER_PRECISION) return newDelay } else { - rdsTripTimestamp.updateArrivalForRealTime(currentDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) + rdsTripTimestamp.updateArrivalForRealTime(currentDelay, rdsSchedule.providerPrecision, PROVIDER_PRECISION) return Duration.ZERO // all delay consumed } } diff --git a/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt index 3dc62331..c749ce97 100644 --- a/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt +++ b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt @@ -829,6 +829,213 @@ class GTFSRealTimeTripUpdatesProviderTests { // end region + // region cancelled timestamps + + @Test + fun test_processRDTripUpdate_trip_cancelled_includeCancelledTimestamps_true() { + val startsAt = DEPARTURE + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + this.scheduleRelationship = GTDScheduleRelationship.CANCELED + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + } + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes)))) } + } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } + + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop, includeCancelledTimestamps = true) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertTrue { timestamp.isCancelled } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertTrue { timestamp.isCancelled } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertTrue { timestamp.isCancelled } + } + } + } + + @Test + fun test_processRDTripUpdate_trip_cancelled_includeCancelledTimestamps_false() { + val startsAt = DEPARTURE + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + this.scheduleRelationship = GTDScheduleRelationship.CANCELED + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + } + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes)))) } + } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } + + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop, includeCancelledTimestamps = false) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + } + + @Test + fun test_processRDTripUpdate_trip_deleted_includeCancelledTimestamps_true() { + val startsAt = DEPARTURE + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + this.scheduleRelationship = GTDScheduleRelationship.DELETED + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + } + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes)))) } + } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } + + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop, includeCancelledTimestamps = true) + + // DELETED trips are always removed even when includeCancelledTimestamps = true + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + } + + @Test + fun test_processRDTripUpdate_stop_skipped_includeCancelledTimestamps_true() { + val startsAt = DEPARTURE + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + } + stopTimeUpdate += stopTimeUpdate { + stopId = "2000" + scheduleRelationship = GTUSTUScheduleRelationship.SKIPPED + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + } + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes)))) } + } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } + + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop, includeCancelledTimestamps = true) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertFalse { timestamp.isCancelled } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertTrue { timestamp.isCancelled } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertFalse { timestamp.isCancelled } + } + } + } + + @Test + fun test_processRDTripUpdate_stop_skipped_includeCancelledTimestamps_false() { + val startsAt = DEPARTURE + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + } + stopTimeUpdate += stopTimeUpdate { + stopId = "2000" + scheduleRelationship = GTUSTUScheduleRelationship.SKIPPED + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + } + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes)))) } + } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } + + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop, includeCancelledTimestamps = false) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertFalse { timestamp.isCancelled } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertFalse { timestamp.isCancelled } + } + } + } + + // end region + private fun Schedule.Timestamp.toSchedule() = mkSchedule( timestamps = listOf(this), )