From a922d999f2277c7fe06fc56665d7a4caac3b4306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 2 Mar 2026 15:44:46 -0500 Subject: [PATCH 01/39] Compat w/ trip updates (status/schedule) Fixes: #80 --- .../android/commons/data/Schedule.java | 13 + .../provider/GTFSRealTimeProvider.java | 123 ++++++- .../provider/gtfs/GtfsRealTimeStorage.kt | 36 +++ .../commons/provider/gtfs/GtfsRealtimeExt.kt | 110 +++++++ .../status/GTFSRealTimeTripUpdatesProvider.kt | 303 ++++++++++++++++++ .../provider/status/StatusProvider.java | 10 + .../provider/status/StatusProviderExt.kt | 73 +++++ src/main/res/values/gtfs_real_time_values.xml | 2 + 8 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt create mode 100644 src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt 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 52a31afa..fc6a7c48 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -839,6 +839,19 @@ public RouteDirectionStop getRouteDirectionStop() { return routeDirectionStop; } + @NonNull + public String getTargetAuthority() { + return this.routeDirectionStop.getAuthority(); + } + + public long getRouteId() { + return this.routeDirectionStop.getRoute().getId(); + } + + public long getDirectionId() { + return this.routeDirectionStop.getDirection().getId(); + } + public long getLookBehindInMsOrDefault() { return lookBehindInMs == null ? LOOK_BEHIND_IN_MS_DEFAULT : lookBehindInMs; } diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index 8f4d3328..fdabbad7 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -33,6 +33,7 @@ import org.mtransit.android.commons.UriUtils; import org.mtransit.android.commons.data.Direction; import org.mtransit.android.commons.data.POI; +import org.mtransit.android.commons.data.POIStatus; import org.mtransit.android.commons.data.Route; import org.mtransit.android.commons.data.ServiceUpdate; import org.mtransit.android.commons.data.Stop; @@ -49,6 +50,9 @@ import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateDbHelper; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProvider; import org.mtransit.android.commons.provider.serviceupdate.ServiceUpdateProviderContract; +import org.mtransit.android.commons.provider.status.GTFSRealTimeTripUpdatesProvider; +import org.mtransit.android.commons.provider.status.StatusProvider; +import org.mtransit.android.commons.provider.status.StatusProviderContract; import org.mtransit.android.commons.provider.vehiclelocations.GTFSRealTimeVehiclePositionsProvider; import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationDbHelper; import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProvider; @@ -91,6 +95,7 @@ @SuppressLint("Registered") public class GTFSRealTimeProvider extends MTContentProvider implements VehicleLocationProviderContract, + StatusProviderContract, ServiceUpdateProviderContract { private static final String LOG_TAG = GTFSRealTimeProvider.class.getSimpleName(); @@ -362,6 +367,53 @@ public static String getAGENCY_VEHICLE_POSITIONS_URL_CACHED(@NonNull Context con return agencyVehiclePositionsUrlCached; } + @Nullable + private static String agencyTripsUrl = null; + + @NonNull + public static String getAgencyTripUpdatesUrlString(@NonNull Context context, @NonNull String token) { + if (agencyTripsUrl == null) { + agencyTripsUrl = getAGENCY_TRIP_UPDATES_URL(context, + token, // 1st (some agency config have only 1 "%s") + MT_HASH_SECRET_AND_DATE + ); + } + return agencyTripsUrl; + } + + @Nullable + private static String agencyTripUpdatesUrl = null; + + /** + * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. + */ + @NonNull + @SuppressLint("StringFormatInvalid") // empty string: set in module app + private static String getAGENCY_TRIP_UPDATES_URL( + @NonNull Context context, + @NonNull String token, + @SuppressWarnings("SameParameterValue") @NonNull String hash + ) { + if (agencyTripUpdatesUrl == null) { + agencyTripUpdatesUrl = context.getResources().getString(R.string.gtfs_real_time_agency_trip_updates_url, token, hash); + } + return agencyTripUpdatesUrl; + } + + @Nullable + private static String agencyTripUpdatesUrlCached = null; + + /** + * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. + */ + @NonNull + public static String getAGENCY_TRIP_UPDATES_URL_CACHED(@NonNull Context context) { + if (agencyTripUpdatesUrlCached == null) { + agencyTripUpdatesUrlCached = context.getResources().getString(R.string.gtfs_real_time_agency_vehicle_positions_url_cached); + } + return agencyTripUpdatesUrlCached; + } + @Nullable private static Boolean ignoreDirection = null; @@ -499,6 +551,63 @@ private static String getAGENCY_TIME_ZONE(@NonNull Context context) { return agencyTimeZone; } + @Override + public long getStatusMaxValidityInMs() { + return GTFSRealTimeTripUpdatesProvider.getMaxValidityInMs(this); + } + + @Override + public long getStatusValidityInMs(boolean inFocus) { + return GTFSRealTimeTripUpdatesProvider.getValidityInMs(this, inFocus); + } + + @Override + public long getMinDurationBetweenRefreshInMs(boolean inFocus) { + return GTFSRealTimeTripUpdatesProvider.getMinDurationBetweenRefreshInMs(this, inFocus); + } + + @Nullable + @Override + public POIStatus getNewStatus(@NonNull StatusProviderContract.Filter statusFilter) { + return GTFSRealTimeTripUpdatesProvider.getNew(this, statusFilter); + } + + @Override + public void cacheStatus(@NonNull POIStatus newStatusToCache) { + StatusProvider.cacheStatusS(this, newStatusToCache); + } + + @Nullable + @Override + public POIStatus getCachedStatus(@NonNull StatusProviderContract.Filter statusFilter) { + return GTFSRealTimeTripUpdatesProvider.getCached(this, statusFilter); + } + + @Override + public boolean purgeUselessCachedStatuses() { + return StatusProvider.purgeUselessCachedStatuses(this); + } + + @Override + public boolean deleteCachedStatus(int cachedStatusId) { + return StatusProvider.deleteCachedStatus(this, cachedStatusId); + } + + public boolean deleteAllCachedStatus() { + return StatusProvider.deleteAllCachedStatus(this); + } + + @Override + public int getStatusType() { + return POI.ITEM_STATUS_TYPE_SCHEDULE; + } + + @NonNull + @Override + public String getStatusDbTableName() { + return GTFSRealTimeDbHelper.T_GTFS_REAL_TIME_TRIP_UPDATES; + } + @SuppressWarnings("unused") @Override public long getMinDurationBetweenVehicleLocationRefreshInMs(boolean inFocus) { @@ -741,6 +850,7 @@ private static String getAgencyServiceAlertsUrlString(@NonNull Context context, private static final ThreadSafeDateFormatter HASH_DATE_FORMATTER; static { + @SuppressWarnings("SpellCheckingInspection") ThreadSafeDateFormatter dateFormatter = new ThreadSafeDateFormatter("yyyyMMdd'T'HHmm'Z'", Locale.ENGLISH); dateFormatter.setTimeZone(UTC_TZ); HASH_DATE_FORMATTER = dateFormatter; @@ -1444,6 +1554,14 @@ public String getLogTag() { */ protected static final String DB_NAME = "gtfsrealtime.db"; + static final String T_GTFS_REAL_TIME_TRIP_UPDATES = StatusProvider.StatusDbHelper.T_STATUS; + + private static final String T_GTFS_REAL_TIME_TRIP_UPDATES_SQL_CREATE = StatusProvider.StatusDbHelper + .getSqlCreateBuilder(T_GTFS_REAL_TIME_TRIP_UPDATES) + .build(); + + private static final String T_GTFS_REAL_TIME_TRIP_UPDATES_SQL_DROP = SqlUtils.getSQLDropIfExistsQuery(T_GTFS_REAL_TIME_TRIP_UPDATES); + static final String T_GTFS_REAL_TIME_VEHICLE_LOCATION = VehicleLocationDbHelper.T_VEHICLE_LOCATION; private static final String T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_CREATE = VehicleLocationDbHelper @@ -1471,8 +1589,9 @@ public static int getDbVersion(@NonNull Context context) { dbVersion++; // add "service_update.original_id" column dbVersion++; // add "vehicle_location" table dbVersion++; // add "vehicle_location.report_timestamp" column - dbVersion++; // change "vehicle_location.[bearing|speed] unit to Int + dbVersion++; // change "vehicle_location.[bearing|speed]" unit to Int dbVersion++; // add "service_update.trip_id" column + dbVersion++; // add "status" table } return dbVersion; } @@ -1491,6 +1610,7 @@ public void onCreateMT(@NonNull SQLiteDatabase db) { @Override public void onUpgradeMT(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL(T_GTFS_REAL_TIME_TRIP_UPDATES_SQL_DROP); db.execSQL(T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_DROP); db.execSQL(T_GTFS_REAL_TIME_SERVICE_UPDATE_SQL_DROP); GtfsRealTimeStorage.saveServiceUpdateLastUpdateMs(context, 0L); @@ -1502,6 +1622,7 @@ public boolean isDbExist(@NonNull Context context) { } private void initAllDbTables(@NonNull SQLiteDatabase db) { + db.execSQL(T_GTFS_REAL_TIME_TRIP_UPDATES_SQL_CREATE); db.execSQL(T_GTFS_REAL_TIME_VEHICLE_LOCATION_SQL_CREATE); db.execSQL(T_GTFS_REAL_TIME_SERVICE_UPDATE_SQL_CREATE); } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt index 9cc08c55..6ee3f087 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealTimeStorage.kt @@ -6,6 +6,42 @@ import org.mtransit.android.commons.PreferenceUtils object GtfsRealTimeStorage { + // region Trip Updates (status schedule) + + /** + * Override if multiple {@link GTFSRealTimeDbHelper} implementations in same app. + */ + private const val PREF_KEY_TRIP_UPDATE_LAST_UPDATE_MS = "pGTFSRealTimeTripUpdatesLastUpdate" + + @JvmStatic + @WorkerThread + fun getTripUpdateLastUpdateMs(context: Context, default: Long) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_TRIP_UPDATE_LAST_UPDATE_MS, default) + + @JvmStatic + @WorkerThread + fun saveTripUpdateLastUpdateMs(context: Context, lastUpdateInMs: Long) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_TRIP_UPDATE_LAST_UPDATE_MS, lastUpdateInMs) + } + + /** + * Override if multiple {@link GTFSRealTimeDbHelper} implementations in same app. + */ + private const val PREF_KEY_TRIP_UPDATE_LAST_UPDATE_CODE = "pGTFSRealTimeTripUpdateLastUpdateCode" + + @JvmStatic + @WorkerThread + fun getTripUpdateLastUpdateCode(context: Context, default: Int) = + PreferenceUtils.getPrefLcl(context, PREF_KEY_TRIP_UPDATE_LAST_UPDATE_CODE, default) + + @JvmStatic + @WorkerThread + fun saveTripUpdateLastUpdateCode(context: Context, code: Int) { + PreferenceUtils.savePrefLclSync(context, PREF_KEY_TRIP_UPDATE_LAST_UPDATE_CODE, code) + } + + // end region + // region Vehicle location /** diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index 99ac21bb..dcc9dba1 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -22,6 +22,26 @@ object GtfsRealtimeExt { } } + @JvmStatic + fun List.toTripUpdates(): List = + this.filter { it.hasVehicle() }.map { it.tripUpdate }.distinct() + + @JvmStatic + fun List.toTripUpdatesWithIdPair(): List> = + this.filter { it.hasVehicle() }.map { it.tripUpdate to it.id }.distinctBy { it.first } + + @JvmStatic + fun List.sortTripUpdates(nowMs: Long = TimeUtils.currentTimeMillis()): List = + this.sortedBy { vehiclePosition -> + vehiclePosition.timestamp + } + + @JvmStatic + fun List>.sortTripUpdatesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = + this.sortedBy { (vehiclePosition, _) -> + vehiclePosition.timestamp + } + @JvmStatic fun List.toVehicles(): List = this.filter { it.hasVehicle() }.map { it.vehicle }.distinct() @@ -118,6 +138,96 @@ object GtfsRealtimeExt { fun GtfsRealtime.TimeRange.endMs(): Long? = this.end.takeIf { this.hasEnd() }?.secToMs() + @JvmStatic + @JvmOverloads + fun GtfsRealtime.TripUpdate.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { + append("TripUpdate:") + append("{") + optTrip?.let { append(it.toStringExt(short = true)).append(", ") } + optVehicle?.let { append(it.toStringExt(short = true)).append(", ") } + optStopTimeUpdateList.let { append(it.toStringExt(short = true)).append(", ") } + optTimestamp?.let { append("timestamp=").append(timestamp).append(", ") } + optDelay?.let { append("delay=").append(delay).append(", ") } + append("}") + } + + val GtfsRealtime.TripUpdate.optTrip get() = if (hasTrip()) trip else null + val GtfsRealtime.TripUpdate.optVehicle get() = if (hasVehicle()) vehicle else null + val GtfsRealtime.TripUpdate.optStopTimeUpdateList get() = stopTimeUpdateList?.takeIf { it.isNotEmpty() } + val GtfsRealtime.TripUpdate.optTimestamp get() = if (hasTimestamp()) timestamp else null + val GtfsRealtime.TripUpdate.optDelay get() = if (hasDelay()) delay else null + val GtfsRealtime.TripUpdate.optTripProperties get() = if (hasTripProperties()) tripProperties else null + + @JvmName("toStringExtStopTimeUpdate") + @JvmStatic + @JvmOverloads + fun List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG) = buildString { + append(if (short) "STUs[" else "StopTimeUpdate[").append(this@toStringExt?.size ?: 0).append("]") + if (debug) { + this@toStringExt?.take(MAX_LIST_ITEMS)?.forEachIndexed { idx, stopTimeUpdate -> + if (idx > 0) append(",") else append("=") + append(stopTimeUpdate.toStringExt(short = true)) + } + } + } + + @JvmStatic + @JvmOverloads + fun GtfsRealtime.TripUpdate.StopTimeUpdate.toStringExt(short: Boolean = false) = buildString { + append(if (short) "STU:" else "StopTimeUpdate:") + append("{") + optStopSequence?.let { append("stopSeq=").append(stopSequence).append(", ") } + optStopId?.let { append("stopId=").append(stopId).append(", ") } + optArrival?.let { append(it.toStringExt(short = true)).append(", ") } + optDeparture?.let { append(it.toStringExt(short = true)).append(", ") } + optDepartureOccupancyStatus?.let { append("depOcc=").append(departureOccupancyStatus).append(", ") } + optScheduleRelationship?.let { append("schedRel=").append(scheduleRelationship).append(", ") } + optStopTimeProperties?.let { append(it.toStringExt(short = true)).append(", ") } + append("}") + } + + val GtfsRealtime.TripUpdate.StopTimeUpdate.optStopSequence get() = if (hasStopSequence()) stopSequence else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.optStopId get() = if (hasStopId()) stopId else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.optArrival get() = if (hasArrival()) arrival else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.optDeparture get() = if (hasDeparture()) departure else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.optDepartureOccupancyStatus get() = if (hasDepartureOccupancyStatus()) departureOccupancyStatus else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.optScheduleRelationship get() = if (hasScheduleRelationship()) scheduleRelationship else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.optStopTimeProperties get() = if (hasStopTimeProperties()) stopTimeProperties else null + + @JvmStatic + @JvmOverloads + fun GtfsRealtime.TripUpdate.StopTimeEvent.toStringExt(short: Boolean = false) = buildString { + append(if (short) "STE:" else "StopTimeEvent:") + append("{") + optDelay?.let { append("delay=").append(delay).append(", ") } + optTime?.let { append("time=").append(time).append(", ") } + optUncertainty?.let { append("uncertainty=").append(uncertainty).append(", ") } + optScheduledTime?.let { append("schedTime=").append(scheduledTime).append(", ") } + append("}") + } + + val GtfsRealtime.TripUpdate.StopTimeEvent.optDelay get() = if (hasDelay()) delay else null + val GtfsRealtime.TripUpdate.StopTimeEvent.optTime get() = if (hasTime()) time else null + val GtfsRealtime.TripUpdate.StopTimeEvent.optUncertainty get() = if (hasUncertainty()) uncertainty else null + val GtfsRealtime.TripUpdate.StopTimeEvent.optScheduledTime get() = if (hasScheduledTime()) scheduledTime else null + + @JvmStatic + @JvmOverloads + fun GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties.toStringExt(short: Boolean = false) = buildString { + append(if (short) "STP:" else "StopTimeProperties:") + append("{") + optAssignedStopId?.let { append("aStopId=").append(assignedStopId).append(", ") } + optStopHeadsign?.let { append("stopHeadsign=").append(stopHeadsign).append(", ") } + optPickupType?.let { append("pickupType=").append(pickupType).append(", ") } + optDropOffType?.let { append("dropOffType=").append(dropOffType).append(", ") } + append("}") + } + + val GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties.optAssignedStopId get() = if (hasAssignedStopId()) assignedStopId else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties.optStopHeadsign get() = if (hasStopHeadsign()) stopHeadsign else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties.optPickupType get() = if (hasPickupType()) pickupType else null + val GtfsRealtime.TripUpdate.StopTimeUpdate.StopTimeProperties.optDropOffType get() = if (hasDropOffType()) dropOffType else null + @JvmStatic @JvmOverloads fun GtfsRealtime.VehiclePosition.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { 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 new file mode 100644 index 00000000..ef037f30 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt @@ -0,0 +1,303 @@ +package org.mtransit.android.commons.provider.status + +import android.content.Context +import android.util.Log +import com.google.transit.realtime.GtfsRealtime +import com.google.transit.realtime.GtfsRealtime.FeedMessage +import org.mtransit.android.commons.Constants +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.SecurityUtils +import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.data.POIStatus +import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.provider.GTFSRealTimeProvider +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteDirectionTagTargetUUID +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteTagTargetUUID +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyTagTargetUUID +import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optBearing +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDirectionId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLabel +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLatitude +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLongitude +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optPosition +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optRouteId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optSpeed +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestamp +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optVehicle +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToHash +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.sortTripUpdates +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toTripUpdates +import org.mtransit.android.commons.provider.gtfs.agencyTag +import org.mtransit.android.commons.provider.gtfs.getTargetUUIDs +import org.mtransit.android.commons.provider.gtfs.getTripIds +import org.mtransit.android.commons.provider.gtfs.makeRequest +import org.mtransit.android.commons.provider.gtfs.routeIdCleanupPattern +import org.mtransit.android.commons.provider.gtfs.tripIdCleanupPattern +import org.mtransit.android.commons.provider.status.StatusProvider.cacheAllStatusesBulkLockDB +import org.mtransit.android.commons.secsToInstant +import java.net.HttpURLConnection +import java.net.SocketException +import java.net.UnknownHostException +import javax.net.ssl.SSLHandshakeException +import kotlin.math.min +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +object GTFSRealTimeTripUpdatesProvider { + + val TRIP_UPDATE_MAX_VALIDITY_IN_MS = 1.hours.inWholeMilliseconds + + val TRIP_UPDATE_VALIDITY_IN_MS = 10.minutes.inWholeMilliseconds + val TRIP_UPDATE_VALIDITY_IN_FOCUS_IN_MS = 10.seconds.inWholeMilliseconds + + @Suppress("unused") + val TRIP_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_MS = 3.minutes.inWholeMilliseconds + + @Suppress("unused") + val TRIP_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS = 1.minutes.inWholeMilliseconds + + @Suppress("unused") + @JvmStatic + fun GTFSRealTimeProvider.getMinDurationBetweenRefreshInMs(inFocus: Boolean) = + if (inFocus) TRIP_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS.adaptForCachedAPI(this.context) + else TRIP_UPDATE_MIN_DURATION_BETWEEN_REFRESH_IN_MS.adaptForCachedAPI(this.context) + + @JvmStatic + fun GTFSRealTimeProvider.getValidityInMs(inFocus: Boolean) = + if (inFocus) TRIP_UPDATE_VALIDITY_IN_FOCUS_IN_MS.adaptForCachedAPI(this.context) + else TRIP_UPDATE_VALIDITY_IN_MS.adaptForCachedAPI(this.context) + + @JvmStatic + val GTFSRealTimeProvider.maxValidityInMs: Long get() = TRIP_UPDATE_MAX_VALIDITY_IN_MS.adaptForCachedAPI(this.context) + + private fun Long.adaptForCachedAPI(context: Context?) = + if (context?.let { GTFSRealTimeProvider.getAGENCY_TRIP_UPDATES_URL_CACHED(it) }?.isNotBlank() == true) { + this * 2L // fewer calls to Cached API $$ + } else this + + @JvmStatic + fun GTFSRealTimeProvider.getCached(statusFilter: StatusProviderContract.Filter): POIStatus? { + val filter = statusFilter as? Schedule.ScheduleStatusFilter ?: run { + MTLog.w(this, "getNewStatus() > Can't find new schedule without schedule filter!") + return null + } + // return (statusFilter as? Schedule.ScheduleStatusFilter)?.let { filter -> + // ( + return filter.routeDirectionStop.getTargetUUIDs(this) + // ?: filter.routeDirection?.getTargetUUIDs(this, includeAgencyTag = INCLUDE_AGENCY_TAG) + // ?: filter.route?.getTargetUUIDs(this, includeAgencyTag = INCLUDE_AGENCY_TAG)) + .let { targetUUIDs -> + val tripIds = filter.targetAuthority.let { targetAuthority -> + filter.routeId.let { routeId -> + context?.getTripIds(targetAuthority, routeId, filter.directionId) + } + } + tripIds + ?.takeIf { tripIds -> tripIds.isNotEmpty() } // trip IDs REQUIRED for GTFS Vehicle locations + ?.let { tripIds -> targetUUIDs to tripIds } + }?.let { (targetUUIDs, tripIds) -> + getCached(targetUUIDs, tripIds) + } + } + + fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List): POIStatus? = + // buildList { + getCachedStatusS(this, targetUUIDs.keys, tripIds) + // ?.let { + // add(it) + // } + // } + ?.let { it.apply { targetUUID = targetUUIDs[it.targetUUID] ?: targetUUID } } + + @JvmStatic + fun GTFSRealTimeProvider.getNew(statusFilter: StatusProviderContract.Filter): POIStatus? { + val filter = statusFilter as? Schedule.ScheduleStatusFilter ?: run { + MTLog.w(this, "getNewStatus() > Can't find new schedule without schedule filter!") + return null + } + updateAgencyDataIfRequired(filter.isInFocusOrDefault) + return getCached(filter) + } + + private fun GTFSRealTimeProvider.updateAgencyDataIfRequired(inFocus: Boolean) { + val context = requireContextCompat() + var inFocus = inFocus + val lastUpdateInMs = GtfsRealTimeStorage.getTripUpdateLastUpdateMs(context, 0L) + val lastUpdateCode = GtfsRealTimeStorage.getTripUpdateLastUpdateCode(context, -1).takeIf { it >= 0 } + if (lastUpdateCode != null && lastUpdateCode != HttpURLConnection.HTTP_OK) { + inFocus = true // force earlier retry if last fetch returned HTTP error + } + val minUpdateMs = min(statusMaxValidityInMs, getStatusValidityInMs(inFocus)) + val nowInMs = TimeUtils.currentTimeMillis() + if (lastUpdateInMs + minUpdateMs > nowInMs) { + return + } + updateAgencyDataIfRequiredSync(lastUpdateInMs, inFocus) + } + + @Synchronized + private fun GTFSRealTimeProvider.updateAgencyDataIfRequiredSync(lastUpdateInMs: Long, inFocus: Boolean) { + val context = requireContextCompat() + if (GtfsRealTimeStorage.getTripUpdateLastUpdateMs(context, 0L) > lastUpdateInMs) { + return // too late, another thread already updated + } + val nowInMs = TimeUtils.currentTimeMillis() + var deleteAllRequired = false + if (lastUpdateInMs + statusMaxValidityInMs < nowInMs) { + deleteAllRequired = true // too old to display + } + val minUpdateMs = min(statusMaxValidityInMs, getStatusValidityInMs(inFocus)) + if (deleteAllRequired || lastUpdateInMs + minUpdateMs < nowInMs) { + updateAllAgencyDataFromWWW(context, deleteAllRequired) // try to update + } + } + + private fun GTFSRealTimeProvider.updateAllAgencyDataFromWWW(context: Context, deleteAllRequired: Boolean) { + var deleteAllDone = false + if (deleteAllRequired) { + deleteAllCachedStatus() + deleteAllDone = true + } + val newStatuses = loadAgencyDataFromWWW(context) + if (newStatuses != null) { // empty is OK + if (!deleteAllDone) { + deleteAllCachedStatus() + } + cacheAllStatusesBulkLockDB(this, newStatuses) + } // else keep whatever we have until max validity reached + } + + private fun GTFSRealTimeProvider.loadAgencyDataFromWWW(context: Context): List? { + try { + val urlRequest = makeRequest( + context, + urlCachedString = GTFSRealTimeProvider.getAGENCY_VEHICLE_POSITIONS_URL_CACHED(context), + getUrlString = { token -> GTFSRealTimeProvider.getAgencyTripUpdatesUrlString(context, token) } + ) ?: return null + getOkHttpClient(context).newCall(urlRequest).execute().use { response -> + GtfsRealTimeStorage.saveTripUpdateLastUpdateCode(context, response.code) + GtfsRealTimeStorage.saveTripUpdateLastUpdateMs(context, TimeUtils.currentTimeMillis()) + when (response.code) { + HttpURLConnection.HTTP_OK -> { + val newLastUpdateInMs = TimeUtils.currentTimeMillis() + val statuses = mutableListOf() + val ignoreDirection = GTFSRealTimeProvider.isIGNORE_DIRECTION(context) + try { + val gFeedMessage = FeedMessage.parseFrom(response.body.bytes()) + val gTripUpdates = gFeedMessage.entityList.toTripUpdates() + for (gTripUpdate in gTripUpdates.sortTripUpdates(newLastUpdateInMs)) { + if (Constants.DEBUG) { + MTLog.d( + this@GTFSRealTimeTripUpdatesProvider, + "loadAgencyDataFromWWW() > GTFS trip updates: ${gTripUpdate.toStringExt()}." + ) + } + processTripUpdates(newLastUpdateInMs, gTripUpdate, ignoreDirection) + ?.takeIf { it.isNotEmpty() } + ?.let { + statuses.addAll(it) + } + } + } catch (e: Exception) { + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, e, "loadAgencyDataFromWWW() > error while parsing GTFS Real Time data!") + } + MTLog.i(this@GTFSRealTimeTripUpdatesProvider, "Found %d vehicle locations.", statuses.size) + if (Constants.DEBUG) { + for (schedule in statuses) { + MTLog.d(this@GTFSRealTimeTripUpdatesProvider, "loadAgencyDataFromWWW() > - new $schedule.") + } + } + return statuses + } + + else -> { + MTLog.w( + this@GTFSRealTimeTripUpdatesProvider, + "ERROR: HTTP URL-Connection Response Code ${response.code} (Message: ${response.message})" + ) + return null + } + } + } + } catch (sslhe: SSLHandshakeException) { + MTLog.w(this, sslhe, "SSL error!") + SecurityUtils.logCertPathValidatorException(sslhe) + GtfsRealTimeStorage.saveTripUpdateLastUpdateCode(context, 567) // SSL certificate not trusted (on this device) + GtfsRealTimeStorage.saveTripUpdateLastUpdateMs(context, TimeUtils.currentTimeMillis()) + return null + } catch (uhe: UnknownHostException) { + if (MTLog.isLoggable(Log.DEBUG)) { + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, uhe, "No Internet Connection!") + } else { + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "No Internet Connection!") + } + return null + } catch (se: SocketException) { + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, se, "No Internet Connection!") + return null + } catch (e: Exception) { // Unknown error + MTLog.e(this@GTFSRealTimeTripUpdatesProvider, e, "INTERNAL ERROR: Unknown Exception") + return null + } + } + + private fun GTFSRealTimeProvider.processTripUpdates( + newLastUpdateInMs: Long, + gTripUpdate: GtfsRealtime.TripUpdate, + ignoreDirection: Boolean, + ): Set? { + val targetUUIDs = parseProviderTargetUUID(gTripUpdate, ignoreDirection)?.takeIf { it.isNotBlank() } ?: return null + return setOf( + Schedule( + authority = this.authority, + targetUUID = targetUUIDs, + targetTripId = gTripUpdate.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern), + lastUpdateInMs = newLastUpdateInMs, + maxValidityInMs = this@processVehiclePositions.vehicleLocationMaxValidityInMs, + // + vehicleId = gTripUpdate.optVehicle?.optId, + vehicleLabel = gTripUpdate.optVehicle?.optLabel, + reportTimestamp = gTripUpdate.optTimestamp?.secsToInstant(), + latitude = gTripUpdate.optPosition?.optLatitude ?: return null, + longitude = gTripUpdate.optPosition?.optLongitude ?: return null, + bearingDegrees = gTripUpdate.optPosition?.optBearing?.toInt(), // in degrees + speedMetersPerSecond = gTripUpdate.optPosition?.optSpeed?.toInt(), // in meters per second + ) + ) + } + + private fun GTFSRealTimeProvider.parseProviderTargetUUID(gTripUpdate: GtfsRealtime.TripUpdate, ignoreDirection: Boolean): String? { + val gTripDescriptor = gTripUpdate.optTrip ?: return null + if (gTripDescriptor.hasModifiedTrip()) { + MTLog.d(this, "parseTargetUUID() > unhandled modified trip: ${gTripDescriptor.toStringExt()}") + } + if (gTripDescriptor.hasStartTime() || gTripDescriptor.hasStartDate()) { + MTLog.d(this, "parseTargetUUID() > unhandled start date & time: ${gTripDescriptor.toStringExt()}") + } + when (gTripDescriptor.scheduleRelationship) { + GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED -> {} // handled + GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.UNSCHEDULED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.REPLACEMENT, + GtfsRealtime.TripDescriptor.ScheduleRelationship.DUPLICATED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.DELETED, + GtfsRealtime.TripDescriptor.ScheduleRelationship.NEW, + -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${gTripDescriptor.scheduleRelationship}") + } + gTripDescriptor.optRouteId?.originalIdToHash(routeIdCleanupPattern)?.let { routeId -> + gTripDescriptor.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> + return getAgencyRouteDirectionTagTargetUUID(agencyTag, routeId, directionId) + } + return getAgencyRouteTagTargetUUID(agencyTag, routeId) + } + return getAgencyTagTargetUUID(agencyTag) + } +} diff --git a/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java index 103e7a72..ca43f06d 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java @@ -252,6 +252,16 @@ public static POIStatus getCachedStatusS(@NonNull StatusProviderContract provide ); } + public static boolean deleteAllCachedStatus(@NonNull StatusProviderContract provider) { + int deletedRows = 0; + try { + deletedRows = provider.getWriteDB().delete(provider.getStatusDbTableName(), null, null); + } catch (Exception e) { + MTLog.w(LOG_TAG, e, "Error while deleting ALL cached statuses!"); + } + return deletedRows > 0; + } + public static boolean deleteCachedStatus(@NonNull StatusProviderContract provider, int cachedStatusId) { String selection = SqlUtils.getWhereEquals(StatusProviderContract.Columns.T_STATUS_K_ID, cachedStatusId); int deletedRows = 0; diff --git a/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt new file mode 100644 index 00000000..051fc02e --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt @@ -0,0 +1,73 @@ +package org.mtransit.android.commons.provider.status + +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.SqlUtils +import org.mtransit.android.commons.data.POIStatus + +private val LOG_TAG: String = StatusProvider::class.java.simpleName + +@JvmOverloads +fun

P.getCachedStatusS( + targetUUID: String, + tripIds: List? = null +) = getCachedStatusS(listOf(targetUUID), tripIds) + +@JvmOverloads +fun

P.getCachedStatusS( + targetUUIDs: Collection, + @Suppress("unused") tripIds: List? = null +): List? { + return getCachedStatusS( + this.contentUri, + buildString { + append(SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_UUID, targetUUIDs)) + // TODO ? if (FeatureFlags.F_USE_TRIP_IS_FOR_STATUSES) { + // tripIds?.takeIf { it.isNotEmpty() }?.let { + // append(SqlUtils.AND) + // append( + // SqlUtils.getWhereGroup( + // SqlUtils.OR, + // SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_TRIP_ID, it), + // SqlUtils.getWhereColumnIsNull(StatusProviderContract.Columns.T_STATUS_K_TARGET_TRIP_ID), + // ) + // ) + // } + // } + } + ) +} + +private fun

P.getCachedStatusS( + @Suppress("unused") uri: Uri?, + selection: String?, +): List? = + try { + SQLiteQueryBuilder() + .apply { + tables = dbTableName + projectionMap = StatusProvider.STATUS_PROJECTION_MAP + }.query( + getReadDB(), StatusProviderContract.PROJECTION_STATUS, selection, null, null, null, null, null + ).use { cursor -> + buildList { + if (cursor != null && cursor.count > 0) { + if (cursor.moveToFirst()) { + do { + add(POIStatus.fromCursor(cursor)) + } while (cursor.moveToNext()) + } + } + } + } + } catch (e: Exception) { + MTLog.w(LOG_TAG, e, "Error!") + null + } + +private val StatusProviderContract.contentUri: Uri + get() = Uri.withAppendedPath(this.authorityUri, StatusProviderContract.STATUS_PATH) + +private val StatusProviderContract.dbTableName: String + get() = this.statusDbTableName diff --git a/src/main/res/values/gtfs_real_time_values.xml b/src/main/res/values/gtfs_real_time_values.xml index 308779d8..f7c867e1 100755 --- a/src/main/res/values/gtfs_real_time_values.xml +++ b/src/main/res/values/gtfs_real_time_values.xml @@ -5,6 +5,8 @@ @string/poi_agency_authority false + + From bc7365a3fe9a8315a4b068e76e5ff1789be5eaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 4 Mar 2026 17:11:44 -0500 Subject: [PATCH 02/39] wip --- .../android/commons/data/Schedule.java | 15 +- .../provider/GTFSRealTimeProvider.java | 2 + .../provider/gtfs/GTFSRDSProviderExt.kt | 43 +++++ .../commons/provider/gtfs/GtfsRealtimeExt.kt | 74 +++++---- .../provider/gtfs/GtfsStatusProviderExt.kt | 49 ++++++ .../status/GTFSRealTimeTripUpdatesProvider.kt | 155 ++++++++++++------ .../provider/status/StatusProvider.java | 2 +- .../provider/status/StatusProviderExt.kt | 36 +--- 8 files changed, 259 insertions(+), 117 deletions(-) create mode 100644 src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsStatusProviderExt.kt 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 fc6a7c48..44830a1c 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -68,10 +68,17 @@ public Schedule(@NonNull POIStatus status, long providerPrecisionInMs, boolean n ); } - public Schedule(@Nullable Integer id, @NonNull String targetUUID, - long lastUpdateInMs, long maxValidityInMs, - long readFromSourceAtInMs, long providerPrecisionInMs, - boolean noPickup, @Nullable String sourceLabel, boolean noData) { + public Schedule( + @Nullable Integer id, + @NonNull String targetUUID, + long lastUpdateInMs, + long maxValidityInMs, + long readFromSourceAtInMs, + long providerPrecisionInMs, + boolean noPickup, + @Nullable String sourceLabel, + boolean noData + ) { super(id, targetUUID, POI.ITEM_STATUS_TYPE_SCHEDULE, lastUpdateInMs, maxValidityInMs, readFromSourceAtInMs, sourceLabel, noData); this.noPickup = noPickup; this.providerPrecisionInMs = providerPrecisionInMs; diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index fdabbad7..e09a01ef 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -569,6 +569,8 @@ public long getMinDurationBetweenRefreshInMs(boolean inFocus) { @Nullable @Override public POIStatus getNewStatus(@NonNull StatusProviderContract.Filter statusFilter) { + this.providedAgencyUrlToken = SecureStringUtils.dec(statusFilter.getProvidedEncryptKey(KeysIds.GTFS_REAL_TIME_URL_TOKEN)); + this.providedAgencyUrlSecret = SecureStringUtils.dec(statusFilter.getProvidedEncryptKey(KeysIds.GTFS_REAL_TIME_URL_SECRET)); return GTFSRealTimeTripUpdatesProvider.getNew(this, statusFilter); } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt index 767a62eb..6f3d52b5 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt @@ -5,8 +5,51 @@ import android.net.Uri import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SqlUtils import org.mtransit.android.commons.UriUtils +import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Trip import org.mtransit.android.commons.provider.GTFSProviderContract +import org.mtransit.android.commons.provider.poi.POIProviderContract + +fun Context.getRDS( + authority: String, + routeId: Long, + directionId: Long? = null, +): List? = try { + contentResolver.query( + Uri.withAppendedPath( + UriUtils.newContentUri(authority), + POIProviderContract.POI_PATH + ), + GTFSProviderContract.PROJECTION_RDS_POI, + buildString { + append( + SqlUtils.getWhereEquals( + GTFSProviderContract.RouteDirectionStopColumns.T_ROUTE_K_ID, + routeId + ) + ) + directionId?.let { + append(SqlUtils.AND) + append(SqlUtils.getWhereEquals(GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_K_ID, it)) + } + }, + null, + SqlUtils.getSortOrderAscending(GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE) + ).use { cursor -> + buildList { + if (cursor != null && cursor.count > 0) { + if (cursor.moveToFirst()) { + do { + add(RouteDirectionStop.fromCursorStatic(cursor, authority)) + } while (cursor.moveToNext()) + } + } + } + } +} catch (e: Exception) { + MTLog.w(this, e, "Error!") + null +} fun Context.getTripIds(authority: String, routeId: Long, directionId: Long? = null) = getTrips(authority, routeId, directionId)?.map { it.tripId } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index dcc9dba1..b7a93720 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -142,13 +142,15 @@ object GtfsRealtimeExt { @JvmOverloads fun GtfsRealtime.TripUpdate.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { append("TripUpdate:") - append("{") - optTrip?.let { append(it.toStringExt(short = true)).append(", ") } - optVehicle?.let { append(it.toStringExt(short = true)).append(", ") } - optStopTimeUpdateList.let { append(it.toStringExt(short = true)).append(", ") } - optTimestamp?.let { append("timestamp=").append(timestamp).append(", ") } - optDelay?.let { append("delay=").append(delay).append(", ") } - append("}") + append( + buildList { + optTrip?.let { add(it.toStringExt(short = true)) } + optVehicle?.let { add(it.toStringExt(short = true)) } + optStopTimeUpdateList?.let { add(it.toStringExt(short = true)) } + optTimestamp?.let { add("timestamp=$timestamp") } + optDelay?.let { add("delay=$delay") } + }.joinToString(separator = ",", prefix = "{", postfix = "}") + ) } val GtfsRealtime.TripUpdate.optTrip get() = if (hasTrip()) trip else null @@ -233,7 +235,7 @@ object GtfsRealtimeExt { fun GtfsRealtime.VehiclePosition.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { append("VehiclePosition:") append("{") - if (hasTrip()) append(trip.toStringExt(short = true)).append(", ") + optTrip?.let { append(it.toStringExt(short = true)).append(", ") } if (hasPosition()) append(position.toStringExt(short = true)).append(", ") if (hasVehicle()) append(vehicle.toStringExt(short = true)).append(", ") if (hasCurrentStopSequence()) append("currentStopSequence=").append(currentStopSequence).append(", ") @@ -275,15 +277,17 @@ object GtfsRealtimeExt { fun GtfsRealtime.VehicleDescriptor.toStringExt(short: Boolean = false) = buildString { append(if (short) "VD:" else "VehicleDescriptor:") append("{") - if (hasId()) append("id=").append(id).append(", ") - if (hasLabel()) append("lbl=").append(label).append(", ") - if (hasLicensePlate()) append("licensePlate=").append(licensePlate).append(", ") - if (hasWheelchairAccessible()) append("a18n=").append(wheelchairAccessible).append(", ") + optId?.let { append("id=").append(id).append(", ") } + optLabel?.let { append("label=").append(label).append(", ") } + optLicensePlate?.let { append("licensePlate=").append(licensePlate).append(", ") } + optWheelchairAccessible?.let { append("a18n=").append(wheelchairAccessible).append(", ") } append("}") } val GtfsRealtime.VehicleDescriptor.optId get() = if (hasId()) id else null val GtfsRealtime.VehicleDescriptor.optLabel get() = if (hasLabel()) label else null + val GtfsRealtime.VehicleDescriptor.optLicensePlate get() = if (hasLicensePlate()) licensePlate else null + val GtfsRealtime.VehicleDescriptor.optWheelchairAccessible get() = if (hasWheelchairAccessible()) wheelchairAccessible else null @JvmStatic @JvmOverloads @@ -305,7 +309,7 @@ object GtfsRealtimeExt { @JvmName("toStringExtEntity") @JvmStatic @JvmOverloads - fun List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG) = buildString { + fun List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG): String = buildString { append(if (short) "ESs[" else "EntitySelectors[").append(this@toStringExt?.size ?: 0).append("]") if (debug) { this@toStringExt?.take(MAX_LIST_ITEMS)?.forEachIndexed { idx, entity -> @@ -350,14 +354,16 @@ object GtfsRealtimeExt { @JvmOverloads fun GtfsRealtime.EntitySelector.toStringExt(short: Boolean = false) = buildString { append(if (short) "ES:" else "EntitySelector:") - append("{") - if (hasAgencyId()) append(if (short) "a=" else "agencyId=").append(agencyId).append("|") - if (hasRouteType()) append(if (short) "rt=" else "routeType=").append(routeType).append("|") - if (hasRouteId()) append(if (short) "r=" else "routeId=").append(routeId).append("|") - if (hasStopId()) append(if (short) "s=" else "stopId=").append(stopId).append("|") - if (hasDirectionId()) append(if (short) "d=" else "directionId=").append(directionId).append("|") - if (hasTrip()) append(trip.toStringExt(short)) - append("}") + append( + buildList { + optAgencyId?.let { add((if (short) "a=" else "agencyId=") + agencyId) } + optRouteType?.let { add((if (short) "rt=" else "routeType=") + routeType) } + optRouteId?.let { add((if (short) "r=" else "routeId=") + routeId) } + optStopId?.let { add((if (short) "s=" else "stopId=") + stopId) } + optDirectionId?.let { add((if (short) "d=" else "directionId=") + directionId) } + optTrip?.let { add(it.toStringExt(short)) } + }.joinToString(separator = "|", prefix = "{", postfix = "}") + ) } val GtfsRealtime.EntitySelector.optAgencyId get() = if (hasAgencyId()) agencyId else null @@ -369,22 +375,28 @@ object GtfsRealtimeExt { @JvmStatic @JvmOverloads - fun GtfsRealtime.TripDescriptor.toStringExt(short: Boolean = false) = buildString { + fun GtfsRealtime.TripDescriptor.toStringExt(short: Boolean = false): String = buildString { append(if (short) "TD:" else "TripDescriptor:") - append("{") - if (hasTripId()) append(if (short) "t=" else "tripId=").append(tripId).append("|") - if (hasDirectionId()) append(if (short) "d=" else "directionId=").append(directionId).append("|") - if (hasRouteId()) append(if (short) "r=" else "routeId=").append(routeId).append("|") - if (hasModifiedTrip()) append(modifiedTrip.toStringExt()) - if (hasScheduleRelationship()) append(if (short) "sr=" else "schedRel=").append(scheduleRelationship).append("|") - if (hasStartDate()) append(if (short) "sd=" else "startDate=").append(startDate).append("|") - if (hasStartTime()) append(if (short) "st=" else "startTime=").append(startTime).append("|") - append("}") + append( + buildList { + optRouteId?.let { add((if (short) "r=" else "routeId=") + routeId) } + optDirectionId?.let { add((if (short) "d=" else "directionId=") + directionId) } + optTripId?.let { add((if (short) "t=" else "tripId=") + tripId) } + optModifiedTrip?.let { add(modifiedTrip.toStringExt()) } + optScheduleRelationship?.let { add((if (short) "sr=" else "schedRel=") + scheduleRelationship) } + optStartDate?.let { add((if (short) "sd=" else "startDate=") + startDate) } + optStartTime?.let { add((if (short) "st=" else "startTime=") + startTime) } + }.joinToString(separator = "|", prefix = "{", postfix = "}") + ) } val GtfsRealtime.TripDescriptor.optTripId get() = if (hasTripId()) tripId else null val GtfsRealtime.TripDescriptor.optRouteId get() = if (hasRouteId()) routeId else null val GtfsRealtime.TripDescriptor.optDirectionId get() = if (hasDirectionId()) directionId else null + val GtfsRealtime.TripDescriptor.optModifiedTrip get() = if (hasModifiedTrip()) modifiedTrip else null + val GtfsRealtime.TripDescriptor.optScheduleRelationship get() = if (hasScheduleRelationship()) scheduleRelationship else null + val GtfsRealtime.TripDescriptor.optStartDate get() = if (hasStartDate()) startDate else null + val GtfsRealtime.TripDescriptor.optStartTime get() = if (hasStartTime()) startTime else null @JvmStatic @JvmOverloads 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 new file mode 100644 index 00000000..4c422047 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsStatusProviderExt.kt @@ -0,0 +1,49 @@ +package org.mtransit.android.commons.provider.gtfs + +import android.content.Context +import android.net.Uri +import org.mtransit.android.commons.MTLog +import org.mtransit.android.commons.SqlUtils +import org.mtransit.android.commons.UriUtils +import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.provider.status.StatusProviderContract + +fun Context.getRDSSchedule( + authority: String, + targetUUID: String, +): Schedule? = getRDSSchedule(authority, listOf(targetUUID))?.singleOrNull() + +fun Context.getRDSSchedule( + authority: String, + targetUUIDs: List, +): List? = try { + contentResolver.query( + Uri.withAppendedPath( + UriUtils.newContentUri(authority), + StatusProviderContract.STATUS_PATH + ), + StatusProviderContract.PROJECTION_STATUS, + buildString { + append( + append(SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_UUID, targetUUIDs)) + ) + }, + null, + null + ).use { cursor -> + buildList { + if (cursor != null && cursor.count > 0) { + if (cursor.moveToFirst()) { + do { + Schedule.fromCursorWithExtra(cursor)?.let { + add(it) + } + } while (cursor.moveToNext()) + } + } + } + } +} catch (e: Exception) { + MTLog.w(this, e, "Error!") + 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 ef037f30..679c1803 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 @@ -9,38 +9,36 @@ import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SecurityUtils import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.data.POIStatus +import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteDirectionTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyTagTargetUUID +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isIGNORE_DIRECTION import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optBearing +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDirectionId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLabel -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLatitude -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLongitude -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optPosition import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optRouteId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optSpeed -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestamp +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optVehicle import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToHash import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.sortTripUpdates import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toTripUpdates import org.mtransit.android.commons.provider.gtfs.agencyTag +import org.mtransit.android.commons.provider.gtfs.getRDS +import org.mtransit.android.commons.provider.gtfs.getRDSSchedule import org.mtransit.android.commons.provider.gtfs.getTargetUUIDs import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.makeRequest import org.mtransit.android.commons.provider.gtfs.routeIdCleanupPattern import org.mtransit.android.commons.provider.gtfs.tripIdCleanupPattern import org.mtransit.android.commons.provider.status.StatusProvider.cacheAllStatusesBulkLockDB -import org.mtransit.android.commons.secsToInstant +import java.io.File +import java.io.IOException import java.net.HttpURLConnection import java.net.SocketException import java.net.UnknownHostException @@ -52,6 +50,8 @@ import kotlin.time.Duration.Companion.seconds object GTFSRealTimeTripUpdatesProvider { + val PROVIDER_PRECISION_IN_MS = 10.seconds.inWholeMilliseconds + val TRIP_UPDATE_MAX_VALIDITY_IN_MS = 1.hours.inWholeMilliseconds val TRIP_UPDATE_VALIDITY_IN_MS = 10.minutes.inWholeMilliseconds @@ -90,7 +90,7 @@ object GTFSRealTimeTripUpdatesProvider { } // return (statusFilter as? Schedule.ScheduleStatusFilter)?.let { filter -> // ( - return filter.routeDirectionStop.getTargetUUIDs(this) + return filter.routeDirectionStop.getTargetUUIDs(this, includeStopTags = true) // ?: filter.routeDirection?.getTargetUUIDs(this, includeAgencyTag = INCLUDE_AGENCY_TAG) // ?: filter.route?.getTargetUUIDs(this, includeAgencyTag = INCLUDE_AGENCY_TAG)) .let { targetUUIDs -> @@ -104,17 +104,60 @@ object GTFSRealTimeTripUpdatesProvider { ?.let { tripIds -> targetUUIDs to tripIds } }?.let { (targetUUIDs, tripIds) -> getCached(targetUUIDs, tripIds) + ?: makeCachedStatusFromAgencyData(filter, tripIds) } } - fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List): POIStatus? = - // buildList { - getCachedStatusS(this, targetUUIDs.keys, tripIds) - // ?.let { - // add(it) - // } - // } - ?.let { it.apply { targetUUID = targetUUIDs[it.targetUUID] ?: targetUUID } } + val GTFSRealTimeProvider.ignoreDirection get() = isIGNORE_DIRECTION(this.requireContextCompat()) + + private fun GTFSRealTimeProvider.makeCachedStatusFromAgencyData(filter: Schedule.ScheduleStatusFilter, tripIds: List): POIStatus? { + val context = context ?: return null + try { + val rds = filter.routeDirectionStop + val targetAuthority = filter.targetAuthority + val routeId = rds.route.id + val directionId = rds.direction.id + var rdsWithSchedule: Map? = null + val gFeedMessage = FeedMessage.parseFrom(File(context.cacheDir, GTFS_RT_TRIP_UPDATE_PB_FILE_NAME).inputStream()) + val gTripUpdates = gFeedMessage.entityList.toTripUpdates() + val rdsTripUpdates = gTripUpdates.mapNotNull { gTripUpdate -> + gTripUpdate.optTrip?.let { it to gTripUpdate } + }.filter { (tripId, _) -> + tripId.optTripId?.originalIdToId(tripIdCleanupPattern)?.let { tripId -> + if (tripId !in tripIds) return@filter false + } + tripId.optRouteId?.originalIdToHash(routeIdCleanupPattern)?.let { routeIdHash -> + if (routeIdHash != rds.route.originalIdHash.toString()) return@filter false + } + tripId.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> + if (directionId != rds.direction.originalDirectionIdOrNull) return@filter false + } + }.takeIf { it.isNotEmpty() } + rdsTripUpdates ?: return null + if (rdsWithSchedule == null) { + rdsWithSchedule = + context.getRDS(this.authority, routeId, directionId) + ?.let { rdsList -> + val allRDSSchedule = context + .getRDSSchedule(targetAuthority, rdsList.map { it.uuid }) + ?.map { + it.targetUUID to it + } + rdsList.associateWith { rds -> + allRDSSchedule?.find { (uuid, _) -> uuid == rds.uuid }?.second + } + } + } + return null + } catch (e: Exception) { + MTLog.w(this, e, "makeCachedStatusFromAgencyData() > error!") + return null + } + } + + fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List): POIStatus? { + return getCachedStatusS(targetUUIDs.keys, tripIds) + } @JvmStatic fun GTFSRealTimeProvider.getNew(statusFilter: StatusProviderContract.Filter): POIStatus? { @@ -174,6 +217,8 @@ object GTFSRealTimeTripUpdatesProvider { } // else keep whatever we have until max validity reached } + private const val GTFS_RT_TRIP_UPDATE_PB_FILE_NAME = "gtfs_rt_trip_update.pb" + private fun GTFSRealTimeProvider.loadAgencyDataFromWWW(context: Context): List? { try { val urlRequest = makeRequest( @@ -186,25 +231,14 @@ object GTFSRealTimeTripUpdatesProvider { GtfsRealTimeStorage.saveTripUpdateLastUpdateMs(context, TimeUtils.currentTimeMillis()) when (response.code) { HttpURLConnection.HTTP_OK -> { - val newLastUpdateInMs = TimeUtils.currentTimeMillis() val statuses = mutableListOf() - val ignoreDirection = GTFSRealTimeProvider.isIGNORE_DIRECTION(context) try { - val gFeedMessage = FeedMessage.parseFrom(response.body.bytes()) - val gTripUpdates = gFeedMessage.entityList.toTripUpdates() - for (gTripUpdate in gTripUpdates.sortTripUpdates(newLastUpdateInMs)) { - if (Constants.DEBUG) { - MTLog.d( - this@GTFSRealTimeTripUpdatesProvider, - "loadAgencyDataFromWWW() > GTFS trip updates: ${gTripUpdate.toStringExt()}." - ) - } - processTripUpdates(newLastUpdateInMs, gTripUpdate, ignoreDirection) - ?.takeIf { it.isNotEmpty() } - ?.let { - statuses.addAll(it) - } + try { + File(context.cacheDir, GTFS_RT_TRIP_UPDATE_PB_FILE_NAME).writeBytes(response.body.bytes()) + } catch (e: IOException) { + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, e, "loadAgencyDataFromWWW() > error while saving GTFS RT Trip Updates data!") } + return null } catch (e: Exception) { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, e, "loadAgencyDataFromWWW() > error while parsing GTFS Real Time data!") } @@ -253,22 +287,47 @@ object GTFSRealTimeTripUpdatesProvider { gTripUpdate: GtfsRealtime.TripUpdate, ignoreDirection: Boolean, ): Set? { + val updateRouteId = gTripUpdate.optTrip?.optRouteId?.originalIdToHash(routeIdCleanupPattern) + val updateDirectionId = gTripUpdate.optTrip?.optDirectionId + ?.takeIf { !ignoreDirection } + val updatedTripId = gTripUpdate.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern) + gTripUpdate.optDelay?.let { + // experimental field, means all stop times are delayed + // -> fetch all trips stops static schedule and generate real-time schedule with delay + } + gTripUpdate.optStopTimeUpdateList?.forEach { stopTimeUpdate -> + stopTimeUpdate.optStopId?.let { stopId -> + // val targetUUIDs = RouteDirectionStop.makeUUID() + } ?: run { + // NO STOP ID provided > not supported, original trip ID "stop sequence" is not in the local DB! + } + } val targetUUIDs = parseProviderTargetUUID(gTripUpdate, ignoreDirection)?.takeIf { it.isNotBlank() } ?: return null return setOf( Schedule( - authority = this.authority, - targetUUID = targetUUIDs, - targetTripId = gTripUpdate.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern), - lastUpdateInMs = newLastUpdateInMs, - maxValidityInMs = this@processVehiclePositions.vehicleLocationMaxValidityInMs, + null, + targetUUIDs, + newLastUpdateInMs, + maxValidityInMs, + newLastUpdateInMs, + PROVIDER_PRECISION_IN_MS, + false, // noPickup + null, // sourceLabel + false // no data // - vehicleId = gTripUpdate.optVehicle?.optId, - vehicleLabel = gTripUpdate.optVehicle?.optLabel, - reportTimestamp = gTripUpdate.optTimestamp?.secsToInstant(), - latitude = gTripUpdate.optPosition?.optLatitude ?: return null, - longitude = gTripUpdate.optPosition?.optLongitude ?: return null, - bearingDegrees = gTripUpdate.optPosition?.optBearing?.toInt(), // in degrees - speedMetersPerSecond = gTripUpdate.optPosition?.optSpeed?.toInt(), // in meters per second + // authority = this.authority, + // targetUUID = targetUUIDs, + // targetTripId = gTripUpdate.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern), + // lastUpdateInMs = newLastUpdateInMs, + // maxValidityInMs = this@processTripUpdates.vehicleLocationMaxValidityInMs, + // // + // vehicleId = gTripUpdate.optVehicle?.optId, + // vehicleLabel = gTripUpdate.optVehicle?.optLabel, + // reportTimestamp = gTripUpdate.optTimestamp?.secsToInstant(), + // latitude = gTripUpdate.optPosition?.optLatitude ?: return null, + // longitude = gTripUpdate.optPosition?.optLongitude ?: return null, + // bearingDegrees = gTripUpdate.optPosition?.optBearing?.toInt(), // in degrees + // speedMetersPerSecond = gTripUpdate.optPosition?.optSpeed?.toInt(), // in meters per second ) ) } diff --git a/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java index ca43f06d..7e85e535 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/status/StatusProvider.java @@ -204,7 +204,7 @@ public static void cacheStatusS(@NonNull StatusProviderContract provider, @NonNu private static final String STATUS_SORT_ORDER = SqlUtils.getSortOrderDescending(Columns.T_STATUS_K_LAST_UPDATE); @Nullable - private static POIStatus getCachedStatusS(@NonNull StatusProviderContract provider, + public static POIStatus getCachedStatusS(@NonNull StatusProviderContract provider, @SuppressWarnings("unused") Uri uri, String selection) { POIStatus cache = null; diff --git a/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt index 051fc02e..c11df2ae 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt @@ -1,13 +1,9 @@ package org.mtransit.android.commons.provider.status -import android.database.sqlite.SQLiteQueryBuilder import android.net.Uri -import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SqlUtils import org.mtransit.android.commons.data.POIStatus -private val LOG_TAG: String = StatusProvider::class.java.simpleName - @JvmOverloads fun

P.getCachedStatusS( targetUUID: String, @@ -18,8 +14,9 @@ fun

P.getCachedStatusS( fun

P.getCachedStatusS( targetUUIDs: Collection, @Suppress("unused") tripIds: List? = null -): List? { - return getCachedStatusS( +): POIStatus? { + return StatusProvider.getCachedStatusS( + this, this.contentUri, buildString { append(SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_UUID, targetUUIDs)) @@ -39,33 +36,6 @@ fun

P.getCachedStatusS( ) } -private fun

P.getCachedStatusS( - @Suppress("unused") uri: Uri?, - selection: String?, -): List? = - try { - SQLiteQueryBuilder() - .apply { - tables = dbTableName - projectionMap = StatusProvider.STATUS_PROJECTION_MAP - }.query( - getReadDB(), StatusProviderContract.PROJECTION_STATUS, selection, null, null, null, null, null - ).use { cursor -> - buildList { - if (cursor != null && cursor.count > 0) { - if (cursor.moveToFirst()) { - do { - add(POIStatus.fromCursor(cursor)) - } while (cursor.moveToNext()) - } - } - } - } - } catch (e: Exception) { - MTLog.w(LOG_TAG, e, "Error!") - null - } - private val StatusProviderContract.contentUri: Uri get() = Uri.withAppendedPath(this.authorityUri, StatusProviderContract.STATUS_PATH) From d3d9d56666627b0ddd09575751a2e26f518e2d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 5 Mar 2026 08:09:27 -0500 Subject: [PATCH 03/39] Update src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../android/commons/provider/gtfs/GtfsRealtimeExt.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index b7a93720..6613e5a9 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -23,12 +23,12 @@ object GtfsRealtimeExt { } @JvmStatic - fun List.toTripUpdates(): List = - this.filter { it.hasVehicle() }.map { it.tripUpdate }.distinct() +fun List.toTripUpdates(): List = + this.filter { it.hasTripUpdate() }.map { it.tripUpdate }.distinct() - @JvmStatic - fun List.toTripUpdatesWithIdPair(): List> = - this.filter { it.hasVehicle() }.map { it.tripUpdate to it.id }.distinctBy { it.first } +@JvmStatic +fun List.toTripUpdatesWithIdPair(): List> = + this.filter { it.hasTripUpdate() }.map { it.tripUpdate to it.id }.distinctBy { it.first } @JvmStatic fun List.sortTripUpdates(nowMs: Long = TimeUtils.currentTimeMillis()): List = From 5be626d149c1a238c23ab39d422ec8221f60e839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 5 Mar 2026 08:09:35 -0500 Subject: [PATCH 04/39] Update src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../mtransit/android/commons/provider/GTFSRealTimeProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index e09a01ef..1878f25f 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -409,7 +409,7 @@ private static String getAGENCY_TRIP_UPDATES_URL( @NonNull public static String getAGENCY_TRIP_UPDATES_URL_CACHED(@NonNull Context context) { if (agencyTripUpdatesUrlCached == null) { - agencyTripUpdatesUrlCached = context.getResources().getString(R.string.gtfs_real_time_agency_vehicle_positions_url_cached); +agencyTripUpdatesUrlCached = context.getResources().getString(R.string.gtfs_real_time_agency_trip_updates_url_cached); } return agencyTripUpdatesUrlCached; } From 0758ff84e75680cfb02009f7f9bf7255973f6abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 6 Mar 2026 11:07:27 -0500 Subject: [PATCH 05/39] wip --- .../mtransit/android/commons/TimeUtilsK.kt | 1 + .../android/commons/data/Schedule.java | 58 +++- .../android/commons/data/ScheduleExt.kt | 30 ++ .../mtransit/android/commons/data/Stop.java | 22 +- .../provider/GTFSRealTimeProvider.java | 19 +- .../provider/gtfs/GTFSRealTimeProviderExt.kt | 28 +- .../gtfs/GTFSScheduleTimestampsProvider.java | 2 +- .../provider/gtfs/GTFSStatusProvider.java | 14 +- .../commons/provider/gtfs/GTFSTripIdsUtils.kt | 6 +- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 2 + .../GTFSRealTimeServiceAlertsProvider.kt | 21 +- .../status/GTFSRealTimeTripUpdatesProvider.kt | 302 ++++++++++++++++-- .../GTFSRealTimeTripUpdatesProviderExt.kt | 107 +++++++ .../GTFSRealTimeVehiclePositionsProvider.kt | 12 +- .../provider/CaLTCOnlineProviderTest.java | 20 +- .../provider/OCTranspoProviderTest.java | 2 +- .../provider/StmInfoApiProviderTests.java | 44 +-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 112 +++++++ 18 files changed, 687 insertions(+), 115 deletions(-) create mode 100644 src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt create mode 100644 src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderExt.kt create mode 100644 src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt diff --git a/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt b/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt index eb737e73..b6814f64 100644 --- a/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt +++ b/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt @@ -26,6 +26,7 @@ object TimeUtilsK { fun Long.millisToInstant() = Instant.fromEpochMilliseconds(this) fun Long.secsToInstant() = Instant.fromEpochSeconds(this) +fun Int.secsToInstant() = this.toLong().secsToInstant() fun Instant.toMillis() = this.toEpochMilliseconds() 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 44830a1c..05222639 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -274,7 +274,7 @@ private void resetUsefulUntilInMs() { this.usefulUntilInMs = 0L; // NOT USEFUL return; } - this.usefulUntilInMs = this.timestamps.get(timestampsCount - 1).t + getUIProviderPrecisionInMs(); + this.usefulUntilInMs = this.timestamps.get(timestampsCount - 1).getDepartureT() + getUIProviderPrecisionInMs(); } private long getUsefulUntilInMs() { @@ -293,8 +293,8 @@ public boolean isUseful() { private static class TimestampComparator implements Comparator { @Override public int compare(Timestamp lhs, Timestamp rhs) { - long lt = lhs == null ? 0L : lhs.t; - long rt = rhs == null ? 0L : rhs.t; + long lt = lhs == null ? 0L : lhs.getDepartureT(); + long rt = rhs == null ? 0L : rhs.getDepartureT(); return (int) (lt - rt); } } @@ -418,7 +418,7 @@ public static JSONObject toJSON(@NonNull Frequency frequency) { } } - public static class Timestamp implements MTLog.Loggable { + public static class Timestamp implements MTLog.Loggable { // Stop Time private static final String LOG_TAG = Timestamp.class.getSimpleName(); @@ -428,7 +428,8 @@ public String getLogTag() { return LOG_TAG; } - public final long t; + @Discouraged(message = "use getDepartureT()/setDepartureT") + public long t; // final @Direction.HeadSignType private int headsignType = Direction.HEADSIGN_TYPE_NONE; @Nullable @@ -442,39 +443,59 @@ public String getLogTag() { @Nullable private Integer accessible = null; @Nullable - private String tripId = null; // will store trip ID int initially but replaced with real trip ID soon after + private String tripId = null; // cleaned trip ID (string) // initial used to store trip id INT but replaced after @Nullable private Long arrivalDiffMs = null; @VisibleForTesting - public Timestamp(long t) { - this.t = t; + public Timestamp(long departureT) { + //noinspection DiscouragedApi + this.t = departureT; } - public Timestamp(long t, @NonNull TimeZone localTimeZone) { - this(t, localTimeZone.getID()); + public Timestamp(long departureT, @NonNull TimeZone localTimeZone) { + this(departureT, localTimeZone.getID()); } - public Timestamp(long t, @NonNull String localTimeZoneId) { - this.t = t; + public Timestamp(long departureT, @NonNull String localTimeZoneId) { + //noinspection DiscouragedApi + this.t = departureT; this.localTimeZoneId = localTimeZoneId; } + @Discouraged(message = "use getDepartureT()") public long getT() { - return t; + return getDepartureT(); + } + + public long getDepartureT() { + //noinspection DiscouragedApi + return this.t; + } + + public void setDepartureT(long departureT) { + final long originalArrivalT = getArrivalT(); // stored as diff -> do not change + //noinspection DiscouragedApi + this.t = departureT; + setArrivalT(originalArrivalT); // stored as diff -> do not change } public long getArrivalT() { - return t + (arrivalDiffMs == null ? 0L : arrivalDiffMs); + return getDepartureT() + (arrivalDiffMs == null ? 0L : arrivalDiffMs); } @Nullable public Long getArrivalTIfDifferent() { - return arrivalDiffMs == null ? null : t + arrivalDiffMs; + return arrivalDiffMs == null ? null : getDepartureT() + arrivalDiffMs; + } + + public void setArrivalT(long arrivalT) { + setArrivalDiffMs(arrivalT - getDepartureT()); } + @Discouraged(message = "use setArrivalT()") public void setArrivalTimestamp(long arrivalTimestamp) { - setArrivalDiffMs(arrivalTimestamp - this.t); + setArrivalDiffMs(arrivalTimestamp - getDepartureT()); } public void setArrivalDiffMs(@Nullable Long arrivalDiffMs) { @@ -650,6 +671,7 @@ public boolean equals(Object o) { Timestamp timestamp = (Timestamp) o; + //noinspection DiscouragedApi if (t != timestamp.t) return false; if (headsignType != timestamp.headsignType) return false; if (!Objects.equals(headsignValue, timestamp.headsignValue)) return false; @@ -665,6 +687,7 @@ public boolean equals(Object o) { @Override public int hashCode() { + //noinspection DiscouragedApi int result = Long.hashCode(t); result = 31 * result + headsignType; result = 31 * result + (headsignValue != null ? headsignValue.hashCode() : 0); @@ -683,7 +706,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(Timestamp.class.getSimpleName()); sb.append('{'); - sb.append("t=").append(Constants.DEBUG ? MTLog.formatDateTime(t) : t); + sb.append("t=").append(Constants.DEBUG ? MTLog.formatDateTime(getDepartureT()) : getDepartureT()); if (arrivalDiffMs != null) { sb.append(", aD:").append(arrivalDiffMs); } @@ -771,6 +794,7 @@ public JSONObject toJSON() { public static JSONObject toJSON(@NonNull Timestamp timestamp) { try { JSONObject jTimestamp = new JSONObject(); + //noinspection DiscouragedApi jTimestamp.put(JSON_TIMESTAMP, timestamp.t); if (timestamp.arrivalDiffMs != null) { jTimestamp.put(JSON_ARRIVAL_DIFF, timestamp.arrivalDiffMs); diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt new file mode 100644 index 00000000..a3467423 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -0,0 +1,30 @@ +package org.mtransit.android.commons.data + +import org.mtransit.android.commons.millisToInstant +import org.mtransit.android.commons.toMillis +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Instant + +fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null): Schedule.Timestamp { + return Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { + arrival?.let { + arrivalT = it.toMillis() + } + } +} + +var Schedule.Timestamp.departure: Instant + get() = departureT.millisToInstant() + set(value) { + departureT = value.toMillis() + } + +@Suppress("unused") +val Schedule.Timestamp.arrivalDiff: Duration? get() = this.arrivalDiffMs?.milliseconds + +var Schedule.Timestamp.arrival: Instant + get() = arrivalT.millisToInstant() + set(value) { + arrivalT = value.toMillis() + } diff --git a/src/main/java/org/mtransit/android/commons/data/Stop.java b/src/main/java/org/mtransit/android/commons/data/Stop.java index d50abaef..9d19f19f 100644 --- a/src/main/java/org/mtransit/android/commons/data/Stop.java +++ b/src/main/java/org/mtransit/android/commons/data/Stop.java @@ -1,5 +1,7 @@ package org.mtransit.android.commons.data; +import static org.mtransit.android.commons.StringUtils.EMPTY; + import android.database.Cursor; import androidx.annotation.NonNull; @@ -186,7 +188,25 @@ public int getAccessible() { } @Nullable - public Integer getOriginalIdHash() { + protected Integer getOriginalIdHash() { return originalIdHash; } + + @Nullable + public String getOriginalIdHashString() { + return originalIdHash == null ? null : String.valueOf(originalIdHash); + } + + @Deprecated + @NonNull + public String getOriginalIdHashStringOrDefault() { + return originalIdHash == null ? EMPTY : String.valueOf(originalIdHash); + } + + public boolean isSameOriginalId(@Nullable String cleanedOriginalIdHash) { + if (cleanedOriginalIdHash == null) return false; + if (this.originalIdHash == null) return false; + return this.originalIdHash.toString().equals(cleanedOriginalIdHash); + + } } diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index 1878f25f..a7d958ae 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -409,7 +409,7 @@ private static String getAGENCY_TRIP_UPDATES_URL( @NonNull public static String getAGENCY_TRIP_UPDATES_URL_CACHED(@NonNull Context context) { if (agencyTripUpdatesUrlCached == null) { -agencyTripUpdatesUrlCached = context.getResources().getString(R.string.gtfs_real_time_agency_trip_updates_url_cached); + agencyTripUpdatesUrlCached = context.getResources().getString(R.string.gtfs_real_time_agency_trip_updates_url_cached); } return agencyTripUpdatesUrlCached; } @@ -736,13 +736,14 @@ public Integer getDirectionTag(@NonNull Direction direction) { return direction.getOriginalDirectionIdOrNull(); } - @NonNull + @Nullable public String getStopTag(@NonNull Stop stop) { - return String.valueOf(stop.getOriginalIdHash()); + return stop.getOriginalIdHashString(); } - @NonNull - public static String getAgencyStopTagTargetUUID(@NonNull String agencyTag, @NonNull String stopTag) { + @Nullable + public static String getAgencyStopTagTargetUUID(@NonNull String agencyTag, @Nullable String stopTag) { + if (stopTag == null) return null; return POI.POIUtils.getUUID(agencyTag, "si" + stopTag); } @@ -751,8 +752,9 @@ public static String getAgencyRouteTagTargetUUID(@NonNull String agencyTag, @Non return POI.POIUtils.getUUID(agencyTag, "ri" + routeTag); } - @NonNull - public static String getAgencyRouteStopTagTargetUUID(@NonNull String agencyTag, @NonNull String routeTag, @NonNull String stopTag) { + @Nullable + public static String getAgencyRouteStopTagTargetUUID(@NonNull String agencyTag, @NonNull String routeTag, @Nullable String stopTag) { + if (stopTag == null) return null; return POI.POIUtils.getUUID(agencyTag, "ri" + routeTag, "si" + stopTag); } @@ -769,8 +771,9 @@ public static String getAgencyRouteDirectionTagTargetUUID(@NonNull String agency } @Nullable - public static String getAgencyRouteDirectionStopTagTargetUUID(@NonNull String agencyTag, @NonNull String routeTag, @Nullable Integer directionTag, @NonNull String stopTag) { + public static String getAgencyRouteDirectionStopTagTargetUUID(@NonNull String agencyTag, @NonNull String routeTag, @Nullable Integer directionTag, @Nullable String stopTag) { if (directionTag == null) return null; + if (stopTag == null) return null; return POI.POIUtils.getUUID(agencyTag, "ri" + routeTag, "d" + directionTag, "si" + stopTag); } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt index 3bfd38b0..dcaef724 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRealTimeProviderExt.kt @@ -21,11 +21,29 @@ import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRoute import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyStopTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isUSE_URL_HASH_SECRET_AND_DATE +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optRouteId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToHash +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToId import java.net.URL +import com.google.transit.realtime.GtfsRealtime.EntitySelector as GEntitySelector +import com.google.transit.realtime.GtfsRealtime.TripDescriptor as GTripDescriptor +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate -val GTFSRealTimeProvider.routeIdCleanupPattern get() = getRouteIdCleanupPattern(requireContextCompat()) -val GTFSRealTimeProvider.tripIdCleanupPattern get() = getTripIdCleanupPattern(requireContextCompat()) -val GTFSRealTimeProvider.stopIdCleanupPattern get() = getStopIdCleanupPattern(requireContextCompat()) +private val GTFSRealTimeProvider.routeIdCleanupPattern get() = getRouteIdCleanupPattern(requireContextCompat()) +fun GTFSRealTimeProvider.parseRouteId(es: GEntitySelector) = es.optRouteId?.let { parseRouteId(it) } +fun GTFSRealTimeProvider.parseRouteId(td: GTripDescriptor) = td.optRouteId?.let { parseRouteId(it) } +fun GTFSRealTimeProvider.parseRouteId(gRouteId: String) = gRouteId.originalIdToHash(routeIdCleanupPattern) + +private val GTFSRealTimeProvider.tripIdCleanupPattern get() = getTripIdCleanupPattern(requireContextCompat()) +fun GTFSRealTimeProvider.parseTripId(td: GTripDescriptor) = td.optTripId?.let { parseTripId(it) } +fun GTFSRealTimeProvider.parseTripId(gTripId: String) = gTripId.originalIdToId(tripIdCleanupPattern) + +private val GTFSRealTimeProvider.stopIdCleanupPattern get() = getStopIdCleanupPattern(requireContextCompat()) +fun GTFSRealTimeProvider.parseStopId(es: GEntitySelector) = es.optStopId?.let { parseStopId(it) } +fun GTFSRealTimeProvider.parseStopId(stu: GTUStopTimeUpdate) = stu.optStopId?.let { parseStopId(it) } +fun GTFSRealTimeProvider.parseStopId(gStopId: String) = gStopId.originalIdToHash(stopIdCleanupPattern) val GTFSRealTimeProvider.agencyTag get() = getAgencyTag(requireContextCompat()) @@ -58,8 +76,8 @@ fun RouteDirectionStop.getTargetUUIDs( getAgencyRouteDirectionStopTagTargetUUID(provider.agencyTag, getRouteTag(provider), getDirectionTag(provider), getStopTag(provider))?.let { put(it, uuid) } - put(getAgencyStopTagTargetUUID(provider.agencyTag, getStopTag(provider)), uuid) - put(getAgencyRouteStopTagTargetUUID(provider.agencyTag, getRouteTag(provider), getStopTag(provider)), uuid) + getAgencyStopTagTargetUUID(provider.agencyTag, getStopTag(provider))?.let { put(it, uuid) } + getAgencyRouteStopTagTargetUUID(provider.agencyTag, getRouteTag(provider), getStopTag(provider))?.let { put(it, uuid) } } } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java index 8e8a7843..4d0c7593 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java @@ -104,7 +104,7 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro } dataRequests++; // 1 more data request done for (Schedule.Timestamp t : dayTimestamps) { - if (t.t >= startsAtInMs && t.t < endsAtInMs) { + if (t.getDepartureT() >= startsAtInMs && t.getDepartureT() < endsAtInMs) { allTimestamps.add(t); } } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java index fa2509a3..a2aee685 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java @@ -273,7 +273,7 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + TWENTY_FOUR_HOURS); } else if (dataRequests == 1) { // ELSE IF today DO - // DO NOTHING (keep now time) + // NOTHING (keep now time) } else { // ELSE IF tomorrow or later DO lookupDayTime = MIDNIGHT; } @@ -305,7 +305,7 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide nbTimestamps += dayTimestamps.size(); } else { for (Schedule.Timestamp dayTimestamp : dayTimestamps) { - if (dayTimestamp.t >= timestamp) { + if (dayTimestamp.getDepartureT() >= timestamp) { nbTimestamps++; } } @@ -396,7 +396,7 @@ private static String getSTOP_SCHEDULE_RAW_FILE_FORMAT(@NonNull Context context) @NonNull static Set findScheduleList( @NonNull GTFSProvider provider, - @SuppressWarnings("unused") long routeId, // included inside direction Id + @SuppressWarnings("unused") long routeId, // included inside direction ID long directionId, // includes routeId, int stopId, String dateS, String timeS, @@ -471,7 +471,7 @@ static Set findScheduleList( if (arrivalDiff > 0) { arrivalTimestampMs = convertToTimestamp(context, lineDeparture - arrivalDiff, dateS); if (arrivalTimestampMs != null) { - timestamp.setArrivalTimestamp(arrivalTimestampMs); + timestamp.setArrivalT(arrivalTimestampMs); } } } @@ -588,7 +588,7 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid if (dataRequests == 0) { // IF yesterday DO override computed date & time with GTFS format for 24+ lookupDayTime = String.valueOf(Integer.parseInt(lookupDayTime) + TWENTY_FOUR_HOURS); } else if (dataRequests == 1) { // ELSE IF today DO - // DO NOTHING (keep now time) + // NOTHING (keep now time) } else { // ELSE IF tomorrow or later DO lookupDayTime = MIDNIGHT; } @@ -738,6 +738,7 @@ private static ThreadSafeDateFormatter getToTimestampFormat(Context context) { return toTimestampFormat; } + @SuppressWarnings("WeakerAccess") @Nullable public static Integer findLastServiceDate(@NonNull GTFSProvider provider) { Integer lastServiceDate = null; @@ -839,7 +840,8 @@ public static Cursor queryS(@NonNull GTFSProvider provider, @NonNull Uri uri, @N return StatusProvider.queryS(provider, uri, selection); } - public static String getSortOrderS(@NonNull GTFSProvider provider, Uri uri) { + @Nullable + public static String getSortOrderS(@NonNull GTFSProvider provider, @NonNull Uri uri) { return StatusProvider.getSortOrderS(provider, uri); } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt index 76f41560..2941d215 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSTripIdsUtils.kt @@ -24,7 +24,8 @@ object GTFSTripIdsUtils : MTLog.Loggable { val idIntToIdMap = loadTripIds(gtfsProvider, tripIdInts) timestamps.forEach { timestamp -> timestamp.tripId?.let { tripIdInt -> - timestamp.tripId = tripIdInt.toIntOrNull()?.let { idIntToIdMap[it] } ?: tripIdInt + timestamp.tripId = tripIdInt.toIntOrNull()?.let { idIntToIdMap[it] } // replace with trip ID (string) + ?: tripIdInt // keep trip ID int if not found // should never happen } } return timestamps @@ -32,11 +33,10 @@ object GTFSTripIdsUtils : MTLog.Loggable { private fun loadTripIds(gtfsProvider: GTFSProvider, tripIdInts: List): Map { if (tripIdInts.isEmpty()) return emptyMap() - val placeholders = tripIdInts.joinToString(",") { "?" } return gtfsProvider.readDB.query( GTFSProviderDbHelper.T_TRIP_IDS, arrayOf(GTFSProviderDbHelper.T_TRIP_IDS_K_ID_INT, GTFSProviderDbHelper.T_TRIP_IDS_K_ID), - "${GTFSProviderDbHelper.T_TRIP_IDS_K_ID_INT} IN ($placeholders)", + "${GTFSProviderDbHelper.T_TRIP_IDS_K_ID_INT} IN (${tripIdInts.joinToString(",") { "?" }})", tripIdInts.toTypedArray(), null, null, diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index 6613e5a9..afca54af 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -3,6 +3,7 @@ package org.mtransit.android.commons.provider.gtfs import com.google.transit.realtime.GtfsRealtime import org.mtransit.android.commons.Constants import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.secsToInstant import org.mtransit.android.toDateTimeLog import org.mtransit.commons.GTFSCommons import org.mtransit.commons.secToMs @@ -210,6 +211,7 @@ fun List.toTripUpdatesWithIdPair(): List + parseRouteId(gEntitySelector)?.let { routeId -> gEntitySelector.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> - gEntitySelector.optStopId?.originalIdToHash(stopIdCleanupPattern)?.let { stopId -> + parseStopId(gEntitySelector)?.let { stopId -> return getAgencyRouteDirectionStopTagTargetUUID(agencyTag, routeId, directionId, stopId) } // no stop return getAgencyRouteDirectionTagTargetUUID(agencyTag, routeId, directionId) } // no direction - gEntitySelector.optStopId?.originalIdToHash(stopIdCleanupPattern)?.let { stopId -> + parseStopId(gEntitySelector)?.let { stopId -> return getAgencyRouteStopTagTargetUUID(agencyTag, routeId, stopId) } return getAgencyRouteTagTargetUUID(agencyTag, routeId) } - gEntitySelector.optStopId?.originalIdToHash(stopIdCleanupPattern)?.let { stopId -> + parseStopId(gEntitySelector)?.let { stopId -> return getAgencyStopTagTargetUUID(agencyTag, stopId) } gEntitySelector.optRouteType?.let { routeType -> 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 679c1803..049938bb 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 @@ -11,21 +11,25 @@ import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.data.POIStatus import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.data.arrival +import org.mtransit.android.commons.data.departure +import org.mtransit.android.commons.millisToInstant import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteDirectionTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isIGNORE_DIRECTION import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optArrival import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDeparture import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDirectionId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optRouteId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToHash -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToId import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toTripUpdates import org.mtransit.android.commons.provider.gtfs.agencyTag @@ -34,8 +38,9 @@ import org.mtransit.android.commons.provider.gtfs.getRDSSchedule import org.mtransit.android.commons.provider.gtfs.getTargetUUIDs import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.makeRequest -import org.mtransit.android.commons.provider.gtfs.routeIdCleanupPattern -import org.mtransit.android.commons.provider.gtfs.tripIdCleanupPattern +import org.mtransit.android.commons.provider.gtfs.parseRouteId +import org.mtransit.android.commons.provider.gtfs.parseStopId +import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.status.StatusProvider.cacheAllStatusesBulkLockDB import java.io.File import java.io.IOException @@ -44,9 +49,11 @@ import java.net.SocketException import java.net.UnknownHostException import javax.net.ssl.SSLHandshakeException import kotlin.math.min +import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant object GTFSRealTimeTripUpdatesProvider { @@ -117,36 +124,134 @@ object GTFSRealTimeTripUpdatesProvider { val targetAuthority = filter.targetAuthority val routeId = rds.route.id val directionId = rds.direction.id - var rdsWithSchedule: Map? = null + var sortedRDS: List? = null + var uuidSchedule: Map? = null val gFeedMessage = FeedMessage.parseFrom(File(context.cacheDir, GTFS_RT_TRIP_UPDATE_PB_FILE_NAME).inputStream()) val gTripUpdates = gFeedMessage.entityList.toTripUpdates() - val rdsTripUpdates = gTripUpdates.mapNotNull { gTripUpdate -> + val rdTripUpdates = gTripUpdates.mapNotNull { gTripUpdate -> gTripUpdate.optTrip?.let { it to gTripUpdate } - }.filter { (tripId, _) -> - tripId.optTripId?.originalIdToId(tripIdCleanupPattern)?.let { tripId -> + }.filter { (trip, _) -> + parseTripId(trip)?.let { tripId -> if (tripId !in tripIds) return@filter false } - tripId.optRouteId?.originalIdToHash(routeIdCleanupPattern)?.let { routeIdHash -> + parseRouteId(trip)?.let { routeIdHash -> if (routeIdHash != rds.route.originalIdHash.toString()) return@filter false } - tripId.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> + trip.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> if (directionId != rds.direction.originalDirectionIdOrNull) return@filter false } + return@filter true }.takeIf { it.isNotEmpty() } - rdsTripUpdates ?: return null - if (rdsWithSchedule == null) { - rdsWithSchedule = - context.getRDS(this.authority, routeId, directionId) + rdTripUpdates ?: return null + if (sortedRDS == null) { + sortedRDS = context.getRDS(this.authority, routeId, directionId) + } + if (uuidSchedule == null) { + uuidSchedule = + sortedRDS ?.let { rdsList -> - val allRDSSchedule = context + context .getRDSSchedule(targetAuthority, rdsList.map { it.uuid }) - ?.map { + ?.associate { it.targetUUID to it } - rdsList.associateWith { rds -> - allRDSSchedule?.find { (uuid, _) -> uuid == rds.uuid }?.second + } + } + if (true) { + uuidSchedule ?: return null + sortedRDS ?: return null + wip(rdTripUpdates, uuidSchedule, sortedRDS) + return null + } + rdTripUpdates.forEach { (trip, gTripUpdate) -> + val updatedTripID = parseTripId(trip) ?: return@forEach + val stopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } + ?: return@forEach + val targetUuidOnThisTrip = uuidSchedule + ?.filter { (_, schedule) -> schedule?.timestamps?.any { it.tripId == updatedTripID } == true } + ?: return@forEach + val sortedRDSOnThisTrip = sortedRDS + ?.filter { rds -> targetUuidOnThisTrip.contains(rds.uuid) } + ?: return@forEach + var currentStopIdHash: String? = null + var currentStopSequence: Int? = null + var currentStopTimeIndex: Int = 0 + var currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) + var nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) + if (true) { + var rdsI = 0 + var stuI = 0 + var currentRDS: RouteDirectionStop? = sortedRDSOnThisTrip.getOrNull(rdsI) ?: return@forEach + var currentStopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate = stopTimeUpdates.getOrNull(stuI) ?: return@forEach + var nextStopTimeUpdate = stopTimeUpdates.getOrNull(stuI + 1) + while (currentRDS != null + && !isSameStop(currentRDS, currentStopTimeUpdate) + && rdsI <= sortedRDSOnThisTrip.size // we do want NULL + ) { + currentRDS = sortedRDSOnThisTrip.getOrNull(++rdsI) + } + currentRDS ?: return@forEach // no match + // 1st trip stop matching 1st stop time update found + var currentRDSTripTimestamp = + targetUuidOnThisTrip[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == updatedTripID } + ?: return@forEach + var (currentArrivalDelay, currentDepartureDelay) = getDelay(currentStopTimeUpdate, currentRDSTripTimestamp) + applyDelay(currentRDSTripTimestamp, currentArrivalDelay, currentDepartureDelay) + currentArrivalDelay = null // only once for the matching stop + currentRDS = sortedRDSOnThisTrip.getOrNull(++rdsI) // NEXT ONE + ?: return@forEach // no more stop + while (currentRDS != null && nextStopTimeUpdate != null + && !isSameStop(currentRDS, nextStopTimeUpdate) + ) { + // keep using current + currentRDSTripTimestamp = + targetUuidOnThisTrip[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == updatedTripID } + ?: continue // FIXME??? + applyDelay(currentRDSTripTimestamp, currentArrivalDelay, currentDepartureDelay) + currentRDS = sortedRDSOnThisTrip.getOrNull(++rdsI) // NEXT ONE + } + currentRDS ?: return@forEach // no more RDS + + currentStopTimeUpdate = stopTimeUpdates.getOrNull(++stuI) ?: return@forEach + nextStopTimeUpdate = stopTimeUpdates.getOrNull(stuI + 1) + getDelay(currentStopTimeUpdate, currentRDSTripTimestamp).let { + currentArrivalDelay = it.first + currentDepartureDelay = it.second + } + return@forEach + } + var generatedStopSequence = 1 + sortedRDSOnThisTrip.forEach { rds -> + generatedStopSequence++ + currentStopIdHash = rds.stop.originalIdHashString + currentStopSequence = generatedStopSequence + if (false) { + findCurrentNextStopTimeUpdate(sortedRDSOnThisTrip, stopTimeUpdates, currentStopIdHash, currentStopSequence, currentStopTimeIndex).let { + currentStopTimeUpdate = it.first + nextStopTimeUpdate = it.second + } + currentStopTimeUpdate?.optStopSequence?.let { currentStopTimeUpdateStopSequence -> + when { + currentStopSequence < currentStopTimeUpdateStopSequence -> return@forEach // no real-time info yet + currentStopSequence > currentStopTimeUpdateStopSequence -> { + nextStopTimeUpdate?.optStopSequence?.let { nextStopTimeUpdateStopSequence -> + if (currentStopSequence < nextStopTimeUpdateStopSequence) { + // keep current stop time update + } else { + currentStopTimeIndex++ + currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) + nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) + } + } // ELSE keep current stop time update + } + + else -> currentStopTimeIndex = 0 } } + } + + + } } return null } catch (e: Exception) { @@ -155,6 +260,154 @@ object GTFSRealTimeTripUpdatesProvider { } } + fun getDelay( + stopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate?, + timestamp: Schedule.Timestamp, + previousDelays: Pair = null to null, + ): Pair { + stopTimeUpdate ?: return null to null // no delay info // show static schedule info + when (stopTimeUpdate.optScheduleRelationship) { + null, // DEFAULT + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SCHEDULED -> { + } // DO NOTHING + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.NO_DATA -> { + // keep static, forget current stop time update + return null to null // no delay info // show static schedule info + } + + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED -> { + // TODO remove trip timestamp (stop will not be stopped ad) + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: SKIPPED") + // return null // stop will be skipped + return previousDelays + } + + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: UNSCHEDULED") + } + } + val timestampOriginalArrival = timestamp.arrival + val timestampOriginalDeparture = timestamp.departure + var departureDelay: Duration? = stopTimeUpdate.optDeparture?.makeDelay(timestampOriginalDeparture) + val arrivalDelay: Duration? = stopTimeUpdate.optArrival?.makeDelay(timestampOriginalArrival) + if (departureDelay == null && arrivalDelay != null) { + departureDelay = timestampOriginalDeparture.coerceAtLeast(timestampOriginalArrival + arrivalDelay) - timestampOriginalDeparture + } + } + + fun applyDelay( + timestamp: Schedule.Timestamp, + arrivalDelay: Duration?, + departureDelay: Duration?, + ) = timestamp.apply { + departureDelay?.let { departure += it } // 1st + arrivalDelay?.let { arrival += it } // 2nd + } + + fun applyUpdate( + timestamp: Schedule.Timestamp, + currentStopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate? + ): Schedule.Timestamp? { + currentStopTimeUpdate ?: return timestamp // no change + when (currentStopTimeUpdate.optScheduleRelationship) { + null, // DEFAULT + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SCHEDULED -> { + } // DO NOTHING + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.NO_DATA -> { + // keep static, forget current stop time update + return timestamp // no change + } + + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED -> { + // TODO remove trip timestamp (stop will not be stopped ad) + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: SKIPPED") + return null // stop will be skipped + } + + GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule + MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: UNSCHEDULED") + } + } + val timestampOriginalArrival = timestamp.arrivalT.millisToInstant() + val timestampOriginalDeparture = timestamp.departureT.millisToInstant() + val departureDelay: Duration? = currentStopTimeUpdate.optDeparture?.makeDelay(timestamp.departureT.millisToInstant()) + val arrivalDelay: Duration? = currentStopTimeUpdate.optArrival?.makeDelay(timestamp.arrivalT.millisToInstant()) + + TODO() + } + + private fun GtfsRealtime.TripUpdate.StopTimeEvent.makeDelay(originalTime: Instant): Duration? = + optDelay?.seconds + ?: optTimeInstant?.let { time -> time - originalTime } + + fun GTFSRealTimeProvider.findCurrentNextStopTimeUpdate( + sortedRDS: List, + stopTimeUpdates: List?, + currentStopIdHash: String?, + currentStopSequence: Int, + currentStopTimeIndex: Int, + ): Pair { + var currentStopTimeIndex = currentStopTimeIndex + var currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) + var nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) + currentStopTimeUpdate?.let { + + } + if (false) { // TODO later stop sequence + currentStopTimeUpdate?.optStopSequence?.let { currentStopTimeUpdateStopSequence -> + while (true) { + if (currentStopSequence < currentStopTimeUpdateStopSequence) { + return null to null // no real-time info yet + } + if (currentStopSequence == currentStopTimeUpdateStopSequence) { + return currentStopTimeUpdate to nextStopTimeUpdate // use + } + if (currentStopSequence > currentStopTimeUpdateStopSequence) { + nextStopTimeUpdate?.optStopSequence?.let { nextStopTimeUpdateStopSequence -> + if (currentStopSequence < nextStopTimeUpdateStopSequence) { + return currentStopTimeUpdate to nextStopTimeUpdate // keep same + } else if (currentStopSequence == nextStopTimeUpdateStopSequence) { + currentStopTimeIndex++ + currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) + nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) + // continue + return currentStopTimeUpdate to nextStopTimeUpdate // use next + } else { + currentStopTimeIndex++ + } + } + } + } + } + } + TODO("Not yet implemented") + } + + fun GTFSRealTimeProvider.getStopTimeUpdateSequence( + sortedRDS: List, + stopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate + ): Int? { + val providedStopSequence = stopTimeUpdate.optStopSequence + val providedStopIdHash = parseStopId(stopTimeUpdate) + if (providedStopSequence == null) { + return sortedRDS.indexOfFirst { rds -> isSameStop(rds, stopTimeUpdate) } + } + var iRDS = 0 + var generatedStopSequence = 1 + while (iRDS < sortedRDS.size) { + // for (; iRDS < sortedRDS.size; iRDS++) { + val currentRDS = sortedRDS[iRDS] + if (isSameStop(currentRDS, stopTimeUpdate)) { + if (generatedStopSequence == providedStopSequence) { + return generatedStopSequence + } + generatedStopSequence++ + } else { + break + } + } + return null + } fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List): POIStatus? { return getCachedStatusS(targetUUIDs.keys, tripIds) } @@ -287,10 +540,10 @@ object GTFSRealTimeTripUpdatesProvider { gTripUpdate: GtfsRealtime.TripUpdate, ignoreDirection: Boolean, ): Set? { - val updateRouteId = gTripUpdate.optTrip?.optRouteId?.originalIdToHash(routeIdCleanupPattern) + val updateRouteId = gTripUpdate.optTrip?.let { parseRouteId(it) } val updateDirectionId = gTripUpdate.optTrip?.optDirectionId ?.takeIf { !ignoreDirection } - val updatedTripId = gTripUpdate.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern) + val updatedTripId = gTripUpdate.optTrip?.let { parseTripId(it) } gTripUpdate.optDelay?.let { // experimental field, means all stop times are delayed // -> fetch all trips stops static schedule and generate real-time schedule with delay @@ -351,7 +604,7 @@ object GTFSRealTimeTripUpdatesProvider { GtfsRealtime.TripDescriptor.ScheduleRelationship.NEW, -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${gTripDescriptor.scheduleRelationship}") } - gTripDescriptor.optRouteId?.originalIdToHash(routeIdCleanupPattern)?.let { routeId -> + parseRouteId(gTripDescriptor)?.let { routeId -> gTripDescriptor.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> return getAgencyRouteDirectionTagTargetUUID(agencyTag, routeId, directionId) } @@ -359,4 +612,7 @@ object GTFSRealTimeTripUpdatesProvider { } return getAgencyTagTargetUUID(agencyTag) } + + private fun GTFSRealTimeProvider.isSameStop(rds: RouteDirectionStop, stopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate) = + rds.stop.isSameOriginalId(parseStopId(stopTimeUpdate)) } 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 new file mode 100644 index 00000000..069530ae --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderExt.kt @@ -0,0 +1,107 @@ +package org.mtransit.android.commons.provider.status + +import org.mtransit.android.commons.data.RouteDirectionStop +import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.data.arrival +import org.mtransit.android.commons.data.departure +import org.mtransit.android.commons.provider.GTFSRealTimeProvider +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId +import org.mtransit.android.commons.provider.gtfs.parseStopId +import org.mtransit.android.commons.provider.gtfs.parseTripId +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import com.google.transit.realtime.GtfsRealtime.TripDescriptor as GTripDescriptor +import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate + + +fun GTFSRealTimeProvider.wip( + rdTripUpdates: List>, + targetUuidSchedule: Map, + sortedRDS: List +) { + rdTripUpdates.forEach { (td, gTripUpdate) -> + val gTripId = td.optTripId ?: return@forEach + val tripId = parseTripId(gTripId) + val tripTargetUuidSchedule = targetUuidSchedule + .filter { (_, schedule) -> schedule?.timestamps?.any { it.tripId == tripId } == true } + .takeIf { it.isNotEmpty() } + ?: return@forEach + val tripSortedRDS = sortedRDS + .filter { rds -> tripTargetUuidSchedule.contains(rds.uuid) } + .takeIf { it.isNotEmpty() } + ?: return@forEach + wipTripUpdate(tripId, gTripUpdate, tripSortedRDS, tripTargetUuidSchedule) + } +} + +private fun GTFSRealTimeProvider.wipTripUpdate( + tripId: String, + gTripUpdate: GTripUpdate, + tripSortedRDS: List, + tripTargetUuidSchedule: Map +) { + var currentDelay = gTripUpdate.optDelay?.seconds // initial delay valid until 1st stop time update + val gStopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } + + var stuIdx = 0 + var currentStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) + var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx + 1) + + var rdsIdx = 0 + var currentRDS: RouteDirectionStop = tripSortedRDS.getOrNull(rdsIdx) + ?: return // no more stop + // ### Iterate on initial stops before 1st stop time update + while (!isSameStop(currentStopTimeUpdate, currentRDS) + && rdsIdx <= tripSortedRDS.size // allow null currentRDS to signify end of trip + ) { + val rdsTripTimestamp = + tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } + currentDelay = wipApplyDelay(rdsTripTimestamp, currentDelay) + currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break + } + currentRDS ?: return // no more stop + // ### use stop time update + TODO() +} + +internal fun wipApplyDelay( + rdsTripTimestamp: Schedule.Timestamp?, + currentDelay: Duration? +): Duration? { + currentDelay ?: return null + rdsTripTimestamp ?: return currentDelay + val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.departure - rdsTripTimestamp.arrival + if (currentDelay < Duration.ZERO) { + rdsTripTimestamp.arrival += currentDelay + rdsTripTimestamp.departure += currentDelay + rdsTripTimestamp.realTime = true + return currentDelay // do not consume negative delay + } else if (currentDiffBetweenArrivalAndDeparture <= currentDelay) { + rdsTripTimestamp.arrival += currentDelay + val newDelay = (currentDelay - currentDiffBetweenArrivalAndDeparture).coerceAtLeast(Duration.ZERO) + rdsTripTimestamp.departure += newDelay + rdsTripTimestamp.realTime = true + return newDelay + } else { + rdsTripTimestamp.arrival += currentDelay + rdsTripTimestamp.realTime = true + return Duration.ZERO // all delay consumed + } +} + + +private fun GTFSRealTimeProvider.isSameStop( + stopTimeUpdate: GTUStopTimeUpdate?, + rds: RouteDirectionStop?, + @Suppress("unused") currentStopSequence: Int? = null, +): Boolean { + stopTimeUpdate ?: return false + rds ?: return false + // TODO check stop sequence as well? + // TODO what about stop present multiple times in same trip? + return rds.stop.isSameOriginalId(parseStopId(stopTimeUpdate)) +} diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt index 3184b691..97908563 100644 --- a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt @@ -20,14 +20,10 @@ import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLabel import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLatitude import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optLongitude import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optPosition -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optRouteId import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optSpeed import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestamp import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optVehicle -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToHash -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.originalIdToId import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.sortVehicles import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toVehicles @@ -35,8 +31,8 @@ import org.mtransit.android.commons.provider.gtfs.agencyTag import org.mtransit.android.commons.provider.gtfs.getTargetUUIDs import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.makeRequest -import org.mtransit.android.commons.provider.gtfs.routeIdCleanupPattern -import org.mtransit.android.commons.provider.gtfs.tripIdCleanupPattern +import org.mtransit.android.commons.provider.gtfs.parseRouteId +import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.vehiclelocations.VehicleLocationProvider.Companion.getCachedVehicleLocationsS import org.mtransit.android.commons.provider.vehiclelocations.model.VehicleLocation import org.mtransit.android.commons.secsToInstant @@ -245,7 +241,7 @@ object GTFSRealTimeVehiclePositionsProvider { VehicleLocation( authority = this.authority, targetUUID = targetUUIDs, - targetTripId = gVehiclePosition.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern), + targetTripId = gVehiclePosition.optTrip?.let { parseTripId(it) }, lastUpdateInMs = newLastUpdateInMs, maxValidityInMs = this@processVehiclePositions.vehicleLocationMaxValidityInMs, // @@ -279,7 +275,7 @@ object GTFSRealTimeVehiclePositionsProvider { GtfsRealtime.TripDescriptor.ScheduleRelationship.NEW, -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${gTripDescriptor.scheduleRelationship}") } - gTripDescriptor.optRouteId?.originalIdToHash(routeIdCleanupPattern)?.let { routeId -> + parseRouteId(gTripDescriptor)?.let { routeId -> gTripDescriptor.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> return getAgencyRouteDirectionTagTargetUUID(agencyTag, routeId, directionId) } diff --git a/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java b/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java index 9c14b462..e937e2de 100644 --- a/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java +++ b/src/test/java/org/mtransit/android/commons/provider/CaLTCOnlineProviderTest.java @@ -117,16 +117,16 @@ public void testParseAgencyJSON() { schedule.getTimestamps().get(0).getHeading()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(52770)), - schedule.getTimestamps().get(0).getT()); + schedule.getTimestamps().get(0).getDepartureT()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(54975)), - schedule.getTimestamps().get(1).getT()); + schedule.getTimestamps().get(1).getDepartureT()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(55215)), - schedule.getTimestamps().get(2).getT()); + schedule.getTimestamps().get(2).getDepartureT()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(55980)), - schedule.getTimestamps().get(3).getT()); + schedule.getTimestamps().get(3).getDepartureT()); } else if (_7_E.equalsIgnoreCase(targetUUID)) { assertEquals( "Argyle Mall Via York", @@ -134,10 +134,10 @@ public void testParseAgencyJSON() { assertEquals(2, schedule.getTimestampsCount()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(52725)), - schedule.getTimestamps().get(0).getT()); + schedule.getTimestamps().get(0).getDepartureT()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(54225)), - schedule.getTimestamps().get(1).getT()); + schedule.getTimestamps().get(1).getDepartureT()); } else if (_17_E.equalsIgnoreCase(targetUUID)) { assertEquals( "Argyle Mall Via Oxford", @@ -145,10 +145,10 @@ public void testParseAgencyJSON() { assertEquals(2, schedule.getTimestampsCount()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(52770)), - schedule.getTimestamps().get(0).getT()); + schedule.getTimestamps().get(0).getDepartureT()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(54495)), - schedule.getTimestamps().get(1).getT()); + schedule.getTimestamps().get(1).getDepartureT()); } else if (_17_W.equalsIgnoreCase(targetUUID)) { assertEquals( "Byron Via Oxford", @@ -156,10 +156,10 @@ public void testParseAgencyJSON() { assertEquals(2, schedule.getTimestampsCount()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(53490)), - schedule.getTimestamps().get(0).getT()); + schedule.getTimestamps().get(0).getDepartureT()); assertEquals( TimeUtils.timeToTheTensSecondsMillis(beginningOfTodayInMs + TimeUnit.SECONDS.toMillis(55215)), - schedule.getTimestamps().get(1).getT()); + schedule.getTimestamps().get(1).getDepartureT()); } else { fail("Unexpected target UUID'" + targetUUID + "'!"); } diff --git a/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java b/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java index f8fa59b7..84d5c833 100644 --- a/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java +++ b/src/test/java/org/mtransit/android/commons/provider/OCTranspoProviderTest.java @@ -215,6 +215,6 @@ public void testParseAgencyJSONArrivalsResults_TwoDirections() { // stop: Lyon # assertNotNull(schedule); assertEquals(3, schedule.getTimestampsCount()); Schedule.Timestamp t0 = schedule.getTimestamps().get(0); - assertEquals("20191221102210", OCTranspoProvider.getDateFormat(context).formatThreadSafe(t0.getT())); + assertEquals("20191221102210", OCTranspoProvider.getDateFormat(context).formatThreadSafe(t0.getDepartureT())); } } \ No newline at end of file diff --git a/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java b/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java index 43583cfd..e48bec1e 100644 --- a/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java +++ b/src/test/java/org/mtransit/android/commons/provider/StmInfoApiProviderTests.java @@ -93,14 +93,14 @@ public void testParseAgencyJSONArrivalsResults() { Schedule schedule = ((Schedule) result.iterator().next()); List timestamps = schedule.getTimestamps(); assertEquals(jResults.size(), timestamps.size()); - assertEquals(1533067680000L, timestamps.get(0).t); - assertEquals(1533068760000L, timestamps.get(1).t); - assertEquals(1533069600000L, timestamps.get(2).t); - assertEquals(1533070500000L, timestamps.get(3).t); - assertEquals(1533071580000L, timestamps.get(4).t); - assertEquals(1533072180000L, timestamps.get(5).t); - assertEquals(1533150000000L, timestamps.get(6).t); - assertEquals(1533154560000L, timestamps.get(7).t); + assertEquals(1533067680000L, timestamps.get(0).getDepartureT()); + assertEquals(1533068760000L, timestamps.get(1).getDepartureT()); + assertEquals(1533069600000L, timestamps.get(2).getDepartureT()); + assertEquals(1533070500000L, timestamps.get(3).getDepartureT()); + assertEquals(1533071580000L, timestamps.get(4).getDepartureT()); + assertEquals(1533072180000L, timestamps.get(5).getDepartureT()); + assertEquals(1533150000000L, timestamps.get(6).getDepartureT()); + assertEquals(1533154560000L, timestamps.get(7).getDepartureT()); } @Test @@ -122,12 +122,12 @@ public void testParseAgencyJSONArrivalsResultsRealTimeNotInMinute() { Schedule schedule = ((Schedule) result.iterator().next()); List timestamps = schedule.getTimestamps(); assertEquals(jResults.size(), timestamps.size()); - assertEquals(1533067680000L, timestamps.get(0).t); - assertEquals(1533068760000L, timestamps.get(1).t); - assertEquals(1533069600000L, timestamps.get(2).t); - assertEquals(1533070500000L, timestamps.get(3).t); - assertEquals(1533071580000L, timestamps.get(4).t); - assertEquals(1533072180000L, timestamps.get(5).t); + assertEquals(1533067680000L, timestamps.get(0).getDepartureT()); + assertEquals(1533068760000L, timestamps.get(1).getDepartureT()); + assertEquals(1533069600000L, timestamps.get(2).getDepartureT()); + assertEquals(1533070500000L, timestamps.get(3).getDepartureT()); + assertEquals(1533071580000L, timestamps.get(4).getDepartureT()); + assertEquals(1533072180000L, timestamps.get(5).getDepartureT()); } @Test @@ -163,13 +163,14 @@ public void testParseAgencyJSONArrivalsResultsInThePastCongestion() { List timestamps = schedule.getTimestamps(); assertEquals(jResults.size(), timestamps.size()); assertTrue(timestamps.get(0).hasHeadsign()); - assertEquals(1538775660000L, timestamps.get(0).t); - assertEquals(1538777700000L, timestamps.get(1).t); - assertEquals(1538779740000L, timestamps.get(2).t); - assertEquals(1538781780000L, timestamps.get(3).t); - assertEquals(1538783760000L, timestamps.get(4).t); + assertEquals(1538775660000L, timestamps.get(0).getDepartureT()); + assertEquals(1538777700000L, timestamps.get(1).getDepartureT()); + assertEquals(1538779740000L, timestamps.get(2).getDepartureT()); + assertEquals(1538781780000L, timestamps.get(3).getDepartureT()); + assertEquals(1538783760000L, timestamps.get(4).getDepartureT()); } + @SuppressWarnings("deprecation") @Test public void testParseAgencyJSONMessageResults() { // Arrange @@ -236,6 +237,7 @@ public void testParseAgencyJSONMessageResults() { serviceUpdate.getTargetUUID()); } + @SuppressWarnings("deprecation") @Test public void testParseAgencyJSONMessageResultsFr() { // Arrange @@ -302,6 +304,7 @@ public void testParseAgencyJSONMessageResultsFr() { serviceUpdate.getTargetUUID()); } + @SuppressWarnings("deprecation") @Test public void testParseAgencyJSONMessageResultsNonStandardDirectionName() { // Arrange @@ -436,6 +439,7 @@ public void testParseAgencyJSONMessageResultsNonStandardDirectionName() { serviceUpdate.getTargetUUID()); } + @SuppressWarnings("deprecation") @Test public void testParseAgencyJSONMessageResultsNonStandardDirectionName2() { // Arrange @@ -522,6 +526,7 @@ public void testParseAgencyJSONMessageResultsNonStandardDirectionName2() { serviceUpdate.getTargetUUID()); } + @SuppressWarnings("deprecation") @Test public void testParseAgencyJSONMessageResultsServiceNormal() { // Arrange @@ -574,6 +579,7 @@ public void testParseAgencyJSONMessageResultsServiceNormal() { assertFalse(ServiceUpdate.isSeverityInfo(serviceUpdate.getSeverity())); } + @SuppressWarnings("deprecation") @Test public void testParseAgencyJSONMessageResultsNoMessages() { // Arrange 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 new file mode 100644 index 00000000..654e160a --- /dev/null +++ b/src/test/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProviderTests.kt @@ -0,0 +1,112 @@ +package org.mtransit.android.commons.provider.status + +import org.mtransit.android.commons.data.arrival +import org.mtransit.android.commons.data.departure +import org.mtransit.android.commons.data.toScheduleTimestamp +import org.mtransit.android.commons.secsToInstant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +class GTFSRealTimeTripUpdatesProviderTests { + + companion object { + private const val LOCAL_TZ_ID: String = "America/Montreal" + + private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: + } + + @Test + fun text_applyDelay_null() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val delay: Duration? = null + + val result = wipApplyDelay(timestamp, delay) + + assertNull(result) + assertFalse { timestamp.isRealTime } + assertEquals(departure, timestamp.departure) + } + + @Test + fun text_applyDelay_0_on_time() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val delay = Duration.ZERO + + val result = wipApplyDelay(timestamp, delay) + + assertNotNull(result) + assertEquals(delay, result) // delay not consumed + assertTrue { timestamp.isRealTime } + assertEquals(departure, timestamp.departure) + } + + @Test + fun text_applyDelay_simple_late() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val delay = 10.minutes + + val result = wipApplyDelay(timestamp, delay) + + assertNotNull(result) + assertEquals(delay, result) // delay not consumed + assertTrue { timestamp.isRealTime } + assertEquals(departure + delay, timestamp.departure) + } + + @Test + fun text_applyDelay_differentArrival_late() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val arrival = departure - 1.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val delay = 10.minutes + + val result = wipApplyDelay(timestamp, delay) + + assertNotNull(result) + assertEquals(9.minutes, result) // delay partially consumed + assertTrue { timestamp.isRealTime } + assertEquals(arrival + delay, timestamp.arrival) + assertEquals(departure + result, timestamp.departure) + } + + @Test + fun text_applyDelay_consumed_late() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val arrival = departure - 15.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val delay = 10.minutes + + val result = wipApplyDelay(timestamp, delay) + + assertNotNull(result) + assertEquals(Duration.ZERO, result) // delay consumed + assertTrue { timestamp.isRealTime } + assertEquals(arrival + delay, timestamp.arrival) + assertEquals(departure, timestamp.departure) + } + + @Test + fun text_applyDelay_simple_early() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val arrival = departure - 1.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val delay = (-5).minutes + + val result = wipApplyDelay(timestamp, delay) + + assertNotNull(result) + assertEquals(delay, result) // delay not consumed + assertTrue { timestamp.isRealTime } + assertEquals(arrival - 5.minutes, timestamp.arrival) + assertEquals(departure - 5.minutes, timestamp.departure) + } +} From d0ebb3448b0ba5ce20476defc93b11d88beac960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 6 Mar 2026 11:16:54 -0500 Subject: [PATCH 06/39] clean --- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 191 ++++++++++-------- .../gtfs/alert/GTFSRTAlertsManager.kt | 32 +-- .../GTFSRealTimeServiceAlertsProvider.kt | 6 +- .../status/GTFSRealTimeTripUpdatesProvider.kt | 71 ++++--- .../GTFSRealTimeVehiclePositionsProvider.kt | 27 +-- .../provider/GTFSRealTimeProviderTest.kt | 25 +-- 6 files changed, 190 insertions(+), 162 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index afca54af..f061d031 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -1,6 +1,5 @@ package org.mtransit.android.commons.provider.gtfs -import com.google.transit.realtime.GtfsRealtime import org.mtransit.android.commons.Constants import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.secsToInstant @@ -8,6 +7,19 @@ import org.mtransit.android.toDateTimeLog import org.mtransit.commons.GTFSCommons import org.mtransit.commons.secToMs import java.util.regex.Pattern +import com.google.transit.realtime.GtfsRealtime.Alert as GAlert +import com.google.transit.realtime.GtfsRealtime.EntitySelector as GEntitySelector +import com.google.transit.realtime.GtfsRealtime.FeedEntity as GFeedEntity +import com.google.transit.realtime.GtfsRealtime.Position as GPosition +import com.google.transit.realtime.GtfsRealtime.TimeRange as GTimeRange +import com.google.transit.realtime.GtfsRealtime.TranslatedString as GTranslatedString +import com.google.transit.realtime.GtfsRealtime.TranslatedString.Translation as GTSTranslation +import com.google.transit.realtime.GtfsRealtime.TripDescriptor as GTripDescriptor +import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate +import com.google.transit.realtime.GtfsRealtime.VehicleDescriptor as GVehicleDescriptor +import com.google.transit.realtime.GtfsRealtime.VehiclePosition as GVehiclePosition @Suppress("MemberVisibilityCanBePrivate", "unused") object GtfsRealtimeExt { @@ -15,7 +27,7 @@ object GtfsRealtimeExt { private const val MAX_LIST_ITEMS: Int = 5 @JvmStatic - fun List.filterUseless(): List { + fun List.filterUseless(): List { return if (this.size <= 1) { this } else { @@ -24,55 +36,55 @@ object GtfsRealtimeExt { } @JvmStatic -fun List.toTripUpdates(): List = +fun List.toTripUpdates(): List = this.filter { it.hasTripUpdate() }.map { it.tripUpdate }.distinct() @JvmStatic -fun List.toTripUpdatesWithIdPair(): List> = +fun List.toTripUpdatesWithIdPair(): List> = this.filter { it.hasTripUpdate() }.map { it.tripUpdate to it.id }.distinctBy { it.first } @JvmStatic - fun List.sortTripUpdates(nowMs: Long = TimeUtils.currentTimeMillis()): List = + fun List.sortTripUpdates(nowMs: Long = TimeUtils.currentTimeMillis()): List = this.sortedBy { vehiclePosition -> vehiclePosition.timestamp } @JvmStatic - fun List>.sortTripUpdatesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = + fun List>.sortTripUpdatesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = this.sortedBy { (vehiclePosition, _) -> vehiclePosition.timestamp } @JvmStatic - fun List.toVehicles(): List = + fun List.toVehicles(): List = this.filter { it.hasVehicle() }.map { it.vehicle }.distinct() @JvmStatic - fun List.toVehiclesWithIdPair(): List> = + fun List.toVehiclesWithIdPair(): List> = this.filter { it.hasVehicle() }.map { it.vehicle to it.id }.distinctBy { it.first } @JvmStatic - fun List.sortVehicles(nowMs: Long = TimeUtils.currentTimeMillis()): List = + fun List.sortVehicles(nowMs: Long = TimeUtils.currentTimeMillis()): List = this.sortedBy { vehiclePosition -> vehiclePosition.timestamp } @JvmStatic - fun List>.sortVehiclesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = + fun List>.sortVehiclesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = this.sortedBy { (vehiclePosition, _) -> vehiclePosition.timestamp } @JvmStatic - fun List.toAlerts(): List = + fun List.toAlerts(): List = this.filter { it.hasAlert() }.map { it.alert }.distinct() @JvmStatic - fun List.toAlertsWithIdPair(): List> = + fun List.toAlertsWithIdPair(): List> = this.filter { it.hasAlert() }.map { it.alert to it.id }.distinctBy { it.first } @JvmStatic - fun List.sortAlerts(nowMs: Long = TimeUtils.currentTimeMillis()): List = + fun List.sortAlerts(nowMs: Long = TimeUtils.currentTimeMillis()): List = this.sortedBy { alert -> (alert.getActivePeriod(nowMs)?.startMs() ?: alert.activePeriodList.firstOrNull { it.hasStart() }?.startMs()) @@ -80,7 +92,7 @@ fun List.toTripUpdatesWithIdPair(): List>.sortAlertsPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = + fun List>.sortAlertsPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = this.sortedBy { (alert, _) -> (alert.getActivePeriod(nowMs)?.startMs() ?: alert.activePeriodList.firstOrNull { it.hasStart() }?.startMs()) @@ -90,7 +102,7 @@ fun List.toTripUpdatesWithIdPair(): List timeRange.isActive(nowMs) } // If multiple ranges are given, the alert will be shown during all of them. @@ -98,22 +110,22 @@ fun List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG) = buildString { + fun List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG) = buildString { append(if (short) "STUs[" else "StopTimeUpdate[").append(this@toStringExt?.size ?: 0).append("]") if (debug) { this@toStringExt?.take(MAX_LIST_ITEMS)?.forEachIndexed { idx, stopTimeUpdate -> @@ -176,7 +189,7 @@ fun List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG): String = buildString { + fun List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG): String = buildString { append(if (short) "ESs[" else "EntitySelectors[").append(this@toStringExt?.size ?: 0).append("]") if (debug) { this@toStringExt?.take(MAX_LIST_ITEMS)?.forEachIndexed { idx, entity -> @@ -324,7 +337,7 @@ fun List.toTripUpdatesWithIdPair(): List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG) = buildString { + fun List?.toStringExt(short: Boolean = false, debug: Boolean = Constants.DEBUG) = buildString { append(if (short) "TRs[" else "TimeRanges[").append(this@toStringExt?.size ?: 0).append("]") if (debug) { this@toStringExt?.take(MAX_LIST_ITEMS)?.forEachIndexed { idx, period -> @@ -337,7 +350,7 @@ fun List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List.toTripUpdatesWithIdPair(): List @@ -425,7 +438,7 @@ fun List.toTripUpdatesWithIdPair(): List infoSeverity - Effect.MODIFIED_SERVICE -> infoSeverity - Effect.REDUCED_SERVICE -> warningSeverity - Effect.NO_SERVICE -> warningSeverity + private fun parseEffectSeverity(gEffect: GAEffect, infoSeverity: Int, warningSeverity: Int): Int = when (gEffect) { + GAEffect.ADDITIONAL_SERVICE -> infoSeverity + GAEffect.MODIFIED_SERVICE -> infoSeverity + GAEffect.REDUCED_SERVICE -> warningSeverity + GAEffect.NO_SERVICE -> warningSeverity - Effect.SIGNIFICANT_DELAYS -> warningSeverity + GAEffect.SIGNIFICANT_DELAYS -> warningSeverity - Effect.DETOUR -> warningSeverity - Effect.STOP_MOVED -> warningSeverity + GAEffect.DETOUR -> warningSeverity + GAEffect.STOP_MOVED -> warningSeverity - Effect.ACCESSIBILITY_ISSUE -> infoSeverity + GAEffect.ACCESSIBILITY_ISSUE -> infoSeverity - Effect.OTHER_EFFECT -> infoSeverity - Effect.UNKNOWN_EFFECT -> infoSeverity - Effect.NO_EFFECT -> infoSeverity + GAEffect.OTHER_EFFECT -> infoSeverity + GAEffect.UNKNOWN_EFFECT -> infoSeverity + GAEffect.NO_EFFECT -> infoSeverity } } \ No newline at end of file diff --git a/src/main/java/org/mtransit/android/commons/provider/serviceupdate/GTFSRealTimeServiceAlertsProvider.kt b/src/main/java/org/mtransit/android/commons/provider/serviceupdate/GTFSRealTimeServiceAlertsProvider.kt index afc323b7..149321e1 100644 --- a/src/main/java/org/mtransit/android/commons/provider/serviceupdate/GTFSRealTimeServiceAlertsProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/serviceupdate/GTFSRealTimeServiceAlertsProvider.kt @@ -1,6 +1,5 @@ package org.mtransit.android.commons.provider.serviceupdate -import com.google.transit.realtime.GtfsRealtime import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.ServiceUpdate @@ -25,6 +24,7 @@ import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.parseRouteId import org.mtransit.android.commons.provider.gtfs.parseStopId import org.mtransit.android.commons.provider.gtfs.parseTripId +import com.google.transit.realtime.GtfsRealtime.EntitySelector as GEntitySelector object GTFSRealTimeServiceAlertsProvider { @@ -58,11 +58,11 @@ object GTFSRealTimeServiceAlertsProvider { } @JvmStatic - fun GTFSRealTimeProvider.parseTargetTripId(gEntitySelector: GtfsRealtime.EntitySelector) = + fun GTFSRealTimeProvider.parseTargetTripId(gEntitySelector: GEntitySelector) = gEntitySelector.optTrip?.let { parseTripId(it) } @JvmStatic - fun GTFSRealTimeProvider.parseProviderTargetUUID(gEntitySelector: GtfsRealtime.EntitySelector, ignoreDirection: Boolean): String? { + fun GTFSRealTimeProvider.parseProviderTargetUUID(gEntitySelector: GEntitySelector, ignoreDirection: Boolean): String? { parseRouteId(gEntitySelector)?.let { routeId -> gEntitySelector.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> parseStopId(gEntitySelector)?.let { stopId -> 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 049938bb..41be6da8 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 @@ -2,8 +2,6 @@ package org.mtransit.android.commons.provider.status import android.content.Context import android.util.Log -import com.google.transit.realtime.GtfsRealtime -import com.google.transit.realtime.GtfsRealtime.FeedMessage import org.mtransit.android.commons.Constants import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SecurityUtils @@ -54,6 +52,11 @@ import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant +import com.google.transit.realtime.GtfsRealtime.FeedMessage as GFeedMessage +import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship +import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate object GTFSRealTimeTripUpdatesProvider { @@ -126,7 +129,7 @@ object GTFSRealTimeTripUpdatesProvider { val directionId = rds.direction.id var sortedRDS: List? = null var uuidSchedule: Map? = null - val gFeedMessage = FeedMessage.parseFrom(File(context.cacheDir, GTFS_RT_TRIP_UPDATE_PB_FILE_NAME).inputStream()) + val gFeedMessage = GFeedMessage.parseFrom(File(context.cacheDir, GTFS_RT_TRIP_UPDATE_PB_FILE_NAME).inputStream()) val gTripUpdates = gFeedMessage.entityList.toTripUpdates() val rdTripUpdates = gTripUpdates.mapNotNull { gTripUpdate -> gTripUpdate.optTrip?.let { it to gTripUpdate } @@ -182,7 +185,7 @@ object GTFSRealTimeTripUpdatesProvider { var rdsI = 0 var stuI = 0 var currentRDS: RouteDirectionStop? = sortedRDSOnThisTrip.getOrNull(rdsI) ?: return@forEach - var currentStopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate = stopTimeUpdates.getOrNull(stuI) ?: return@forEach + var currentStopTimeUpdate: GTUStopTimeUpdate = stopTimeUpdates.getOrNull(stuI) ?: return@forEach var nextStopTimeUpdate = stopTimeUpdates.getOrNull(stuI + 1) while (currentRDS != null && !isSameStop(currentRDS, currentStopTimeUpdate) @@ -261,28 +264,28 @@ object GTFSRealTimeTripUpdatesProvider { } fun getDelay( - stopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate?, + stopTimeUpdate: GTUStopTimeUpdate?, timestamp: Schedule.Timestamp, previousDelays: Pair = null to null, ): Pair { stopTimeUpdate ?: return null to null // no delay info // show static schedule info when (stopTimeUpdate.optScheduleRelationship) { null, // DEFAULT - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SCHEDULED -> { + GTUStopTimeUpdate.ScheduleRelationship.SCHEDULED -> { } // DO NOTHING - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.NO_DATA -> { + GTUStopTimeUpdate.ScheduleRelationship.NO_DATA -> { // keep static, forget current stop time update return null to null // no delay info // show static schedule info } - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED -> { + GTUStopTimeUpdate.ScheduleRelationship.SKIPPED -> { // TODO remove trip timestamp (stop will not be stopped ad) MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: SKIPPED") // return null // stop will be skipped return previousDelays } - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule + GTUStopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: UNSCHEDULED") } } @@ -293,6 +296,7 @@ object GTFSRealTimeTripUpdatesProvider { if (departureDelay == null && arrivalDelay != null) { departureDelay = timestampOriginalDeparture.coerceAtLeast(timestampOriginalArrival + arrivalDelay) - timestampOriginalDeparture } + return arrivalDelay to departureDelay } fun applyDelay( @@ -306,25 +310,25 @@ object GTFSRealTimeTripUpdatesProvider { fun applyUpdate( timestamp: Schedule.Timestamp, - currentStopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate? + currentStopTimeUpdate: GTUStopTimeUpdate? ): Schedule.Timestamp? { currentStopTimeUpdate ?: return timestamp // no change when (currentStopTimeUpdate.optScheduleRelationship) { null, // DEFAULT - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SCHEDULED -> { + GTUStopTimeUpdate.ScheduleRelationship.SCHEDULED -> { } // DO NOTHING - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.NO_DATA -> { + GTUStopTimeUpdate.ScheduleRelationship.NO_DATA -> { // keep static, forget current stop time update return timestamp // no change } - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED -> { + GTUStopTimeUpdate.ScheduleRelationship.SKIPPED -> { // TODO remove trip timestamp (stop will not be stopped ad) MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: SKIPPED") return null // stop will be skipped } - GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule + GTUStopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: UNSCHEDULED") } } @@ -336,17 +340,17 @@ object GTFSRealTimeTripUpdatesProvider { TODO() } - private fun GtfsRealtime.TripUpdate.StopTimeEvent.makeDelay(originalTime: Instant): Duration? = + private fun GTUStopTimeEvent.makeDelay(originalTime: Instant): Duration? = optDelay?.seconds ?: optTimeInstant?.let { time -> time - originalTime } fun GTFSRealTimeProvider.findCurrentNextStopTimeUpdate( sortedRDS: List, - stopTimeUpdates: List?, + stopTimeUpdates: List?, currentStopIdHash: String?, currentStopSequence: Int, currentStopTimeIndex: Int, - ): Pair { + ): Pair { var currentStopTimeIndex = currentStopTimeIndex var currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) var nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) @@ -385,13 +389,21 @@ object GTFSRealTimeTripUpdatesProvider { fun GTFSRealTimeProvider.getStopTimeUpdateSequence( sortedRDS: List, - stopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate + stopTimeUpdate: GTUStopTimeUpdate ): Int? { val providedStopSequence = stopTimeUpdate.optStopSequence val providedStopIdHash = parseStopId(stopTimeUpdate) + // .optStopId?.originalIdToHash(stopIdCleanupPattern) + ?: return providedStopSequence if (providedStopSequence == null) { return sortedRDS.indexOfFirst { rds -> isSameStop(rds, stopTimeUpdate) } } + // providedStopSequence?.let { + // return it + // } + // TODO HERE NOW, it's complicated, trip stop sequence can be a mess + // TODO: only guarantee is stop order... if stop not repeated in same trip + // TODO -> maybe start simple first by using stop ID if available then stop sequence and ignore complex use case data for now var iRDS = 0 var generatedStopSequence = 1 while (iRDS < sortedRDS.size) { @@ -408,6 +420,7 @@ object GTFSRealTimeTripUpdatesProvider { } return null } + fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List): POIStatus? { return getCachedStatusS(targetUUIDs.keys, tripIds) } @@ -537,7 +550,7 @@ object GTFSRealTimeTripUpdatesProvider { private fun GTFSRealTimeProvider.processTripUpdates( newLastUpdateInMs: Long, - gTripUpdate: GtfsRealtime.TripUpdate, + gTripUpdate: GTripUpdate, ignoreDirection: Boolean, ): Set? { val updateRouteId = gTripUpdate.optTrip?.let { parseRouteId(it) } @@ -585,7 +598,7 @@ object GTFSRealTimeTripUpdatesProvider { ) } - private fun GTFSRealTimeProvider.parseProviderTargetUUID(gTripUpdate: GtfsRealtime.TripUpdate, ignoreDirection: Boolean): String? { + private fun GTFSRealTimeProvider.parseProviderTargetUUID(gTripUpdate: GTripUpdate, ignoreDirection: Boolean): String? { val gTripDescriptor = gTripUpdate.optTrip ?: return null if (gTripDescriptor.hasModifiedTrip()) { MTLog.d(this, "parseTargetUUID() > unhandled modified trip: ${gTripDescriptor.toStringExt()}") @@ -594,14 +607,14 @@ object GTFSRealTimeTripUpdatesProvider { MTLog.d(this, "parseTargetUUID() > unhandled start date & time: ${gTripDescriptor.toStringExt()}") } when (gTripDescriptor.scheduleRelationship) { - GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED -> {} // handled - GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.UNSCHEDULED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.REPLACEMENT, - GtfsRealtime.TripDescriptor.ScheduleRelationship.DUPLICATED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.DELETED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.NEW, + GTDScheduleRelationship.SCHEDULED -> {} // handled + GTDScheduleRelationship.ADDED, + GTDScheduleRelationship.UNSCHEDULED, + GTDScheduleRelationship.CANCELED, + GTDScheduleRelationship.REPLACEMENT, + GTDScheduleRelationship.DUPLICATED, + GTDScheduleRelationship.DELETED, + GTDScheduleRelationship.NEW, -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${gTripDescriptor.scheduleRelationship}") } parseRouteId(gTripDescriptor)?.let { routeId -> @@ -613,6 +626,6 @@ object GTFSRealTimeTripUpdatesProvider { return getAgencyTagTargetUUID(agencyTag) } - private fun GTFSRealTimeProvider.isSameStop(rds: RouteDirectionStop, stopTimeUpdate: GtfsRealtime.TripUpdate.StopTimeUpdate) = + private fun GTFSRealTimeProvider.isSameStop(rds: RouteDirectionStop, stopTimeUpdate: GTUStopTimeUpdate) = rds.stop.isSameOriginalId(parseStopId(stopTimeUpdate)) } diff --git a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt index 97908563..b65d64ed 100644 --- a/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/vehiclelocations/GTFSRealTimeVehiclePositionsProvider.kt @@ -1,8 +1,6 @@ package org.mtransit.android.commons.provider.vehiclelocations import android.content.Context -import com.google.transit.realtime.GtfsRealtime -import com.google.transit.realtime.GtfsRealtime.FeedMessage import org.mtransit.android.commons.Constants import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SecurityUtils @@ -44,6 +42,9 @@ import kotlin.math.min import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import com.google.transit.realtime.GtfsRealtime.FeedMessage as GFeedMessage +import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship +import com.google.transit.realtime.GtfsRealtime.VehiclePosition as GVehiclePosition object GTFSRealTimeVehiclePositionsProvider { @@ -173,7 +174,7 @@ object GTFSRealTimeVehiclePositionsProvider { val vehicleLocations = mutableListOf() val ignoreDirection = GTFSRealTimeProvider.isIGNORE_DIRECTION(context) try { - val gFeedMessage = FeedMessage.parseFrom(response.body.bytes()) + val gFeedMessage = GFeedMessage.parseFrom(response.body.bytes()) val gVehiclePositions = gFeedMessage.entityList.toVehicles() for (gVehiclePosition in gVehiclePositions.sortVehicles(newLastUpdateInMs)) { if (Constants.DEBUG) { @@ -233,7 +234,7 @@ object GTFSRealTimeVehiclePositionsProvider { private fun GTFSRealTimeProvider.processVehiclePositions( newLastUpdateInMs: Long, - gVehiclePosition: GtfsRealtime.VehiclePosition, + gVehiclePosition: GVehiclePosition, ignoreDirection: Boolean, ): Set? { val targetUUIDs = parseProviderTargetUUID(gVehiclePosition, ignoreDirection)?.takeIf { it.isNotBlank() } ?: return null @@ -256,7 +257,7 @@ object GTFSRealTimeVehiclePositionsProvider { ) } - private fun GTFSRealTimeProvider.parseProviderTargetUUID(gVehiclePosition: GtfsRealtime.VehiclePosition, ignoreDirection: Boolean): String? { + private fun GTFSRealTimeProvider.parseProviderTargetUUID(gVehiclePosition: GVehiclePosition, ignoreDirection: Boolean): String? { val gTripDescriptor = gVehiclePosition.optTrip ?: return null if (gTripDescriptor.hasModifiedTrip()) { MTLog.d(this, "parseTargetUUID() > unhandled modified trip: ${gTripDescriptor.toStringExt()}") @@ -265,14 +266,14 @@ object GTFSRealTimeVehiclePositionsProvider { MTLog.d(this, "parseTargetUUID() > unhandled start date & time: ${gTripDescriptor.toStringExt()}") } when (gTripDescriptor.scheduleRelationship) { - GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED -> {} // handled - GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.UNSCHEDULED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.REPLACEMENT, - GtfsRealtime.TripDescriptor.ScheduleRelationship.DUPLICATED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.DELETED, - GtfsRealtime.TripDescriptor.ScheduleRelationship.NEW, + GTDScheduleRelationship.SCHEDULED -> {} // handled + GTDScheduleRelationship.ADDED, + GTDScheduleRelationship.UNSCHEDULED, + GTDScheduleRelationship.CANCELED, + GTDScheduleRelationship.REPLACEMENT, + GTDScheduleRelationship.DUPLICATED, + GTDScheduleRelationship.DELETED, + GTDScheduleRelationship.NEW, -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${gTripDescriptor.scheduleRelationship}") } parseRouteId(gTripDescriptor)?.let { routeId -> diff --git a/src/test/java/org/mtransit/android/commons/provider/GTFSRealTimeProviderTest.kt b/src/test/java/org/mtransit/android/commons/provider/GTFSRealTimeProviderTest.kt index b1cb929b..6561b1b6 100644 --- a/src/test/java/org/mtransit/android/commons/provider/GTFSRealTimeProviderTest.kt +++ b/src/test/java/org/mtransit/android/commons/provider/GTFSRealTimeProviderTest.kt @@ -1,6 +1,5 @@ package org.mtransit.android.commons.provider -import com.google.transit.realtime.GtfsRealtime import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -10,6 +9,8 @@ import org.mockito.kotlin.whenever import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.isActive import org.mtransit.commons.msToSec +import com.google.transit.realtime.GtfsRealtime.Alert as GAlert +import com.google.transit.realtime.GtfsRealtime.TimeRange as GTimeRange class GTFSRealTimeProviderTest { @@ -40,8 +41,8 @@ class GTFSRealTimeProviderTest { @Test fun testIsInActivePeriod_InRange() { val nowInMs = TimeUtils.currentTimeMillis() - val gAlert = GtfsRealtime.Alert.newBuilder() - .addActivePeriod(mock().apply { + val gAlert = GAlert.newBuilder() + .addActivePeriod(mock().apply { whenever(hasStart()).thenReturn(true) whenever(start).thenReturn((nowInMs - 1000L).msToSec()) whenever(hasEnd()).thenReturn(true) @@ -57,8 +58,8 @@ class GTFSRealTimeProviderTest { @Test fun testIsInActivePeriod_InRange_StartOnly() { val nowInMs = TimeUtils.currentTimeMillis() - val gAlert = GtfsRealtime.Alert.newBuilder() - .addActivePeriod(mock().apply { + val gAlert = GAlert.newBuilder() + .addActivePeriod(mock().apply { whenever(hasStart()).thenReturn(true) whenever(start).thenReturn((nowInMs - 1000L).msToSec()) whenever(hasEnd()).thenReturn(false) @@ -73,8 +74,8 @@ class GTFSRealTimeProviderTest { @Test fun testIsInActivePeriod_InRange_EndOnly() { val nowInMs = TimeUtils.currentTimeMillis() - val gAlert = GtfsRealtime.Alert.newBuilder() - .addActivePeriod(mock().apply { + val gAlert = GAlert.newBuilder() + .addActivePeriod(mock().apply { whenever(hasStart()).thenReturn(false) whenever(hasEnd()).thenReturn(true) whenever(end).thenReturn((nowInMs + 1000L).msToSec()) @@ -89,8 +90,8 @@ class GTFSRealTimeProviderTest { @Test fun testIsInActivePeriod_OutRange_Before() { val nowInMs = TimeUtils.currentTimeMillis() - val gAlert = GtfsRealtime.Alert.newBuilder() - .addActivePeriod(mock().apply { + val gAlert = GAlert.newBuilder() + .addActivePeriod(mock().apply { whenever(hasStart()).thenReturn(true) whenever(start).thenReturn((nowInMs - 2000L).msToSec()) whenever(hasEnd()).thenReturn(true) @@ -106,8 +107,8 @@ class GTFSRealTimeProviderTest { @Test fun testIsInActivePeriod_OutRange_After() { val nowInMs = TimeUtils.currentTimeMillis() - val gAlert = GtfsRealtime.Alert.newBuilder() - .addActivePeriod(mock().apply { + val gAlert = GAlert.newBuilder() + .addActivePeriod(mock().apply { whenever(hasStart()).thenReturn(true) whenever(start).thenReturn((nowInMs + 1000L).msToSec()) whenever(hasEnd()).thenReturn(true) @@ -123,7 +124,7 @@ class GTFSRealTimeProviderTest { // https://gtfs.org/realtime/feed-entities/service-alerts/#timerange @Test fun testIsInActivePeriod_0_Range() { - val gAlert = GtfsRealtime.Alert.newBuilder() + val gAlert = GAlert.newBuilder() // no active period .buildPartial() From 211e766bde367d7af4da44ba812c68a5f1f1e497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 6 Mar 2026 11:56:28 -0500 Subject: [PATCH 07/39] wip --- .../android/commons/data/Schedule.java | 2 +- .../android/commons/data/ScheduleExt.kt | 2 +- .../GTFSRealTimeTripUpdatesProviderExt.kt | 43 ++++++++++++-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 57 +++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) 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 05222639..28636d6f 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -490,7 +490,7 @@ public Long getArrivalTIfDifferent() { } public void setArrivalT(long arrivalT) { - setArrivalDiffMs(arrivalT - getDepartureT()); + setArrivalDiffMs(getDepartureT() - arrivalT); } @Discouraged(message = "use setArrivalT()") diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index a3467423..20f91e5a 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -21,7 +21,7 @@ var Schedule.Timestamp.departure: Instant } @Suppress("unused") -val Schedule.Timestamp.arrivalDiff: Duration? get() = this.arrivalDiffMs?.milliseconds +val Schedule.Timestamp.arrivalDiff: Duration? get() = this.arrivalDiffMs?.milliseconds?.coerceAtLeast(Duration.ZERO) var Schedule.Timestamp.arrival: Instant get() = arrivalT.millisToInstant() 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 069530ae..9bc454d8 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 @@ -1,20 +1,26 @@ package org.mtransit.android.commons.provider.status +import com.google.transit.realtime.arrivalOrNull +import com.google.transit.realtime.departureOrNull import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.data.arrival +import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId import org.mtransit.android.commons.provider.gtfs.parseStopId import org.mtransit.android.commons.provider.gtfs.parseTripId import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant import com.google.transit.realtime.GtfsRealtime.TripDescriptor as GTripDescriptor import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate @@ -50,7 +56,7 @@ private fun GTFSRealTimeProvider.wipTripUpdate( var stuIdx = 0 var currentStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx + 1) - + var rdsTripTimestamp: Schedule.Timestamp? = null var rdsIdx = 0 var currentRDS: RouteDirectionStop = tripSortedRDS.getOrNull(rdsIdx) ?: return // no more stop @@ -58,16 +64,45 @@ private fun GTFSRealTimeProvider.wipTripUpdate( while (!isSameStop(currentStopTimeUpdate, currentRDS) && rdsIdx <= tripSortedRDS.size // allow null currentRDS to signify end of trip ) { - val rdsTripTimestamp = - tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } + rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } currentDelay = wipApplyDelay(rdsTripTimestamp, currentDelay) currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break } - currentRDS ?: return // no more stop + if (rdsIdx >= tripSortedRDS.size) return // no more stop + currentStopTimeUpdate ?: return // no more stop time update // ### use stop time update + rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } + currentDelay = wipApplyDelay2(rdsTripTimestamp, currentStopTimeUpdate) TODO() } +internal fun wipApplyDelay2( + rdsTripTimestamp: Schedule.Timestamp?, + currentStopTimeUpdate: GTUStopTimeUpdate +): Duration? { + rdsTripTimestamp ?: return null // impossible to handle + val timestampOriginalArrival = rdsTripTimestamp.arrival + val timestampOriginalDeparture = rdsTripTimestamp.departure + val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO + val stuArrivalDelay = currentStopTimeUpdate.arrivalOrNull.wipMakeDelay(timestampOriginalArrival) + val stuDepartureDelay = currentStopTimeUpdate.departureOrNull.wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) + TODO() +} + +internal fun GTUStopTimeEvent?.wipMakeDelay( + originalTime: Instant, + previousDelay: Duration? = null, + previousOriginalDiff: Duration? = null, +): Duration? { + return this?.optDelay?.seconds + ?: this?.optTimeInstant?.let { time -> time - originalTime } + ?: previousDelay?.let { + previousOriginalDiff?.let { + (previousDelay - previousOriginalDiff).coerceAtLeast(Duration.ZERO) + } + } +} + internal fun wipApplyDelay( rdsTripTimestamp: Schedule.Timestamp?, currentDelay: Duration? 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 654e160a..a52ff5cb 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 @@ -1,9 +1,12 @@ package org.mtransit.android.commons.provider.status +import com.google.transit.realtime.TripUpdateKt.stopTimeEvent import org.mtransit.android.commons.data.arrival +import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.data.toScheduleTimestamp import org.mtransit.android.commons.secsToInstant +import org.mtransit.android.commons.toSecs import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -12,6 +15,8 @@ import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent class GTFSRealTimeTripUpdatesProviderTests { @@ -21,6 +26,8 @@ class GTFSRealTimeTripUpdatesProviderTests { private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: } + // region applyDelay + @Test fun text_applyDelay_null() { val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET @@ -109,4 +116,54 @@ class GTFSRealTimeTripUpdatesProviderTests { assertEquals(arrival - 5.minutes, timestamp.arrival) assertEquals(departure - 5.minutes, timestamp.departure) } + + // endregion + + // region makeDelay + + @Test + fun test_makeDelay_1() { + val originalTime = DEPARTURE_MS.secsToInstant() + val stopTimeEvent = stopTimeEvent { + delay = 10 + } + + val result = stopTimeEvent.wipMakeDelay(originalTime) + + assertNotNull(result) + assertEquals(10.seconds, result) + } + + @Test + fun test_makeDelay_2() { + val originalTime = DEPARTURE_MS.secsToInstant() + val stopTimeEvent = stopTimeEvent { + time = (originalTime + 10.seconds).toSecs() + } + + val result = stopTimeEvent.wipMakeDelay(originalTime) + + assertNotNull(result) + assertEquals(10.seconds, result) + } + + @Test + fun test_makeDelay_3() { + val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val arrival = departure - 5.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val previousDelay = 10.minutes + val stopTimeEvent: GTUStopTimeEvent? = null + + val result = stopTimeEvent.wipMakeDelay( + originalTime = timestamp.departure, + previousDelay = previousDelay, + previousOriginalDiff = timestamp.arrivalDiff + ) + + assertNotNull(result) + assertEquals(5.minutes, result) + } + + // endregion } From ebe9fd9356e02d21db897cf188aead99f626554a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 6 Mar 2026 14:19:22 -0500 Subject: [PATCH 08/39] wip --- .../android/commons/data/Schedule.java | 6 ++--- .../android/commons/data/ScheduleExt.kt | 4 +-- .../android/commons/data/ScheduleExtTests.kt | 25 +++++++++++++++++++ .../GTFSRealTimeTripUpdatesProviderTests.kt | 14 +++++------ 4 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt 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 28636d6f..81d2eb24 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -481,12 +481,12 @@ public void setDepartureT(long departureT) { } public long getArrivalT() { - return getDepartureT() + (arrivalDiffMs == null ? 0L : arrivalDiffMs); + return getDepartureT() - (arrivalDiffMs == null ? 0L : arrivalDiffMs); } @Nullable public Long getArrivalTIfDifferent() { - return arrivalDiffMs == null ? null : getDepartureT() + arrivalDiffMs; + return arrivalDiffMs == null ? null : getDepartureT() - arrivalDiffMs; } public void setArrivalT(long arrivalT) { @@ -495,7 +495,7 @@ public void setArrivalT(long arrivalT) { @Discouraged(message = "use setArrivalT()") public void setArrivalTimestamp(long arrivalTimestamp) { - setArrivalDiffMs(arrivalTimestamp - getDepartureT()); + setArrivalDiffMs(getDepartureT() - arrivalTimestamp); } public void setArrivalDiffMs(@Nullable Long arrivalDiffMs) { diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 20f91e5a..e949c01c 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -8,9 +8,7 @@ import kotlin.time.Instant fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null): Schedule.Timestamp { return Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { - arrival?.let { - arrivalT = it.toMillis() - } + arrival?.let { this.arrival = it } } } diff --git a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt new file mode 100644 index 00000000..3438c66e --- /dev/null +++ b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt @@ -0,0 +1,25 @@ +package org.mtransit.android.commons.data + +import org.mtransit.android.commons.secsToInstant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.minutes + +class ScheduleExtTests { + + companion object { + private const val LOCAL_TZ_ID: String = "America/Montreal" + private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: + } + + @Test + fun test1() { + val departure = DEPARTURE_MS.secsToInstant() + val arrival = departure - 1.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + + timestamp.departure += 1.minutes + assertEquals(arrival, timestamp.arrival) + assertEquals(departure + 1.minutes, timestamp.departure) + } +} 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 a52ff5cb..66e94ab7 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 @@ -30,7 +30,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_null() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) val delay: Duration? = null @@ -43,7 +43,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_0_on_time() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) val delay = Duration.ZERO @@ -57,7 +57,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_simple_late() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) val delay = 10.minutes @@ -71,7 +71,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_differentArrival_late() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) val delay = 10.minutes @@ -87,7 +87,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_consumed_late() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 15.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) val delay = 10.minutes @@ -103,7 +103,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_simple_early() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) val delay = (-5).minutes @@ -149,7 +149,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_makeDelay_3() { - val departure = DEPARTURE_MS.secsToInstant() // 2026-03-06 10:00:00 ET + val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) val previousDelay = 10.minutes From a60907da079a30beb163fc49180ec7de010fc7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 6 Mar 2026 14:41:46 -0500 Subject: [PATCH 09/39] wip --- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 31 +++---- .../GTFSRealTimeTripUpdatesProviderExt.kt | 18 ++-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 88 +++++++++++++++++++ 3 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index f061d031..3d7dfb27 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -7,6 +7,8 @@ import org.mtransit.android.toDateTimeLog import org.mtransit.commons.GTFSCommons import org.mtransit.commons.secToMs import java.util.regex.Pattern +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import com.google.transit.realtime.GtfsRealtime.Alert as GAlert import com.google.transit.realtime.GtfsRealtime.EntitySelector as GEntitySelector import com.google.transit.realtime.GtfsRealtime.FeedEntity as GFeedEntity @@ -36,24 +38,20 @@ object GtfsRealtimeExt { } @JvmStatic -fun List.toTripUpdates(): List = - this.filter { it.hasTripUpdate() }.map { it.tripUpdate }.distinct() + fun List.toTripUpdates(): List = + this.filter { it.hasTripUpdate() }.map { it.tripUpdate }.distinct() -@JvmStatic -fun List.toTripUpdatesWithIdPair(): List> = - this.filter { it.hasTripUpdate() }.map { it.tripUpdate to it.id }.distinctBy { it.first } + @JvmStatic + fun List.toTripUpdatesWithIdPair(): List> = + this.filter { it.hasTripUpdate() }.map { it.tripUpdate to it.id }.distinctBy { it.first } @JvmStatic fun List.sortTripUpdates(nowMs: Long = TimeUtils.currentTimeMillis()): List = - this.sortedBy { vehiclePosition -> - vehiclePosition.timestamp - } + this.sortedBy { it.timestamp } @JvmStatic fun List>.sortTripUpdatesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = - this.sortedBy { (vehiclePosition, _) -> - vehiclePosition.timestamp - } + this.sortedBy { (it, _) -> it.timestamp } @JvmStatic fun List.toVehicles(): List = @@ -65,15 +63,11 @@ fun List.toTripUpdatesWithIdPair(): List> @JvmStatic fun List.sortVehicles(nowMs: Long = TimeUtils.currentTimeMillis()): List = - this.sortedBy { vehiclePosition -> - vehiclePosition.timestamp - } + this.sortedBy { it.timestamp } @JvmStatic fun List>.sortVehiclesPair(nowMs: Long = TimeUtils.currentTimeMillis()): List> = - this.sortedBy { (vehiclePosition, _) -> - vehiclePosition.timestamp - } + this.sortedBy { (vehiclePosition, _) -> vehiclePosition.timestamp } @JvmStatic fun List.toAlerts(): List = @@ -171,7 +165,7 @@ fun List.toTripUpdatesWithIdPair(): List> val GTripUpdate.optStopTimeUpdateList get() = stopTimeUpdateList?.takeIf { it.isNotEmpty() } val GTripUpdate.optTimestamp get() = if (hasTimestamp()) timestamp else null val GTripUpdate.optDelay get() = if (hasDelay()) delay else null - + val GTripUpdate.optDelayDuration get() = this.optDelay?.seconds val GTripUpdate.optTripProperties get() = if (hasTripProperties()) tripProperties else null @JvmName("toStringExtStopTimeUpdate") @@ -223,6 +217,7 @@ fun List.toTripUpdatesWithIdPair(): List> } val GTUStopTimeEvent.optDelay get() = if (hasDelay()) delay else null + val GTUStopTimeEvent.optDelayDuration: Duration? get() = this.optDelay?.seconds val GTUStopTimeEvent.optTime get() = if (hasTime()) time else null val GTUStopTimeEvent.optTimeInstant get() = if (hasTime()) time.secsToInstant() else null val GTUStopTimeEvent.optUncertainty get() = if (hasUncertainty()) uncertainty else null 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 9bc454d8..00064662 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 @@ -72,21 +72,27 @@ private fun GTFSRealTimeProvider.wipTripUpdate( currentStopTimeUpdate ?: return // no more stop time update // ### use stop time update rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } - currentDelay = wipApplyDelay2(rdsTripTimestamp, currentStopTimeUpdate) + currentDelay = wipApplyDelaySTU(rdsTripTimestamp, currentStopTimeUpdate, currentDelay) TODO() } -internal fun wipApplyDelay2( +internal fun wipApplyDelaySTU( rdsTripTimestamp: Schedule.Timestamp?, - currentStopTimeUpdate: GTUStopTimeUpdate + currentStopTimeUpdate: GTUStopTimeUpdate, + currentDelay: Duration? = null, ): Duration? { rdsTripTimestamp ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO - val stuArrivalDelay = currentStopTimeUpdate.arrivalOrNull.wipMakeDelay(timestampOriginalArrival) - val stuDepartureDelay = currentStopTimeUpdate.departureOrNull.wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) - TODO() + val stuArrivalDelay = currentStopTimeUpdate.arrivalOrNull + .wipMakeDelay(timestampOriginalArrival) + ?: currentDelay + val stuDepartureDelay = currentStopTimeUpdate.departureOrNull + .wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) + stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } + stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } + return stuDepartureDelay } internal fun GTUStopTimeEvent?.wipMakeDelay( 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 66e94ab7..05d93b71 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 @@ -1,6 +1,7 @@ package org.mtransit.android.commons.provider.status import com.google.transit.realtime.TripUpdateKt.stopTimeEvent +import com.google.transit.realtime.TripUpdateKt.stopTimeUpdate import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure @@ -119,6 +120,93 @@ class GTFSRealTimeTripUpdatesProviderTests { // endregion + // region applyDelaySTU + + @Test + fun text_applyDelaySTU_simple() { + val departure = DEPARTURE_MS.secsToInstant() + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val stopTimeUpdate = stopTimeUpdate { + this.departure = stopTimeEvent { + time = (departure + 1.minutes).toSecs() + } + } + + val result = wipApplyDelaySTU(timestamp, stopTimeUpdate) + + assertNotNull(result) + assertEquals(1.minutes, result) + assertTrue { timestamp.isRealTime } + assertEquals(departure + 1.minutes, timestamp.departure) + } + + @Test + fun text_applyDelaySTU_2() { + val departure = DEPARTURE_MS.secsToInstant() + val arrival = departure - 5.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val stopTimeUpdate = stopTimeUpdate { + this.arrival = stopTimeEvent { + time = (arrival - 1.minutes).toSecs() + } + this.departure = stopTimeEvent { + time = (departure + 2.minutes).toSecs() + } + } + + val result = wipApplyDelaySTU(timestamp, stopTimeUpdate) + + assertNotNull(result) + assertEquals(2.minutes, result) + assertTrue { timestamp.isRealTime } + assertEquals(arrival - 1.minutes, timestamp.arrival) + assertEquals(departure + 2.minutes, timestamp.departure) + } + + @Test + fun text_applyDelaySTU_3() { + val departure = DEPARTURE_MS.secsToInstant() + val arrival = departure - 5.minutes + val delay = 1.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val stopTimeUpdate = stopTimeUpdate { + this.departure = stopTimeEvent { + time = (departure + 2.minutes).toSecs() + } + } + + val result = wipApplyDelaySTU(timestamp, stopTimeUpdate, delay) + + assertNotNull(result) + assertEquals(2.minutes, result) + assertTrue { timestamp.isRealTime } + assertEquals(arrival + 1.minutes, timestamp.arrival) + assertEquals(departure + 2.minutes, timestamp.departure) + } + + @Test + fun text_applyDelaySTU_4() { + val departure = DEPARTURE_MS.secsToInstant() + val arrival = departure - 1.minutes + val delay = 15.minutes // should be ignored + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val stopTimeUpdate = stopTimeUpdate { + this.arrival = stopTimeEvent { + time = (arrival + 3.minutes).toSecs() + } + } + + val result = wipApplyDelaySTU(timestamp, stopTimeUpdate, delay) + + assertNotNull(result) + assertEquals(2.minutes, result) + assertTrue { timestamp.isRealTime } + assertEquals(arrival + 3.minutes, timestamp.arrival) + assertEquals(departure + 2.minutes, timestamp.departure) + } + + // endregion + // region makeDelay @Test From 0e1fd25980bde03675b402aaaafd1e4420b800d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 6 Mar 2026 16:13:22 -0500 Subject: [PATCH 10/39] wip --- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 22 ++ .../GTFSRealTimeTripUpdatesProviderExt.kt | 45 ++-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 227 ++++++++++++++++++ 3 files changed, 274 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index 3d7dfb27..09bca551 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -1,5 +1,7 @@ package org.mtransit.android.commons.provider.gtfs +import com.google.transit.realtime.TripUpdateKt +import com.google.transit.realtime.TripUpdateKt.StopTimeEventKt import org.mtransit.android.commons.Constants import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.secsToInstant @@ -436,4 +438,24 @@ object GtfsRealtimeExt { fun GTSTranslation.toStringExt() = buildString { append("{").append(language).append(":").append(text).append("}") } + + var TripUpdateKt.Dsl.delayDuration: Duration? + get() = this.delay.takeIf { hasDelay() }?.seconds + set(value) { + value?.inWholeSeconds?.toInt()?.let { + this.delay = it + } ?: run { + this.clearDelay() + } + } + + var StopTimeEventKt.Dsl.delayDuration: Duration? + get() = this.delay.takeIf { hasDelay() }?.seconds + set(value) { + value?.inWholeSeconds?.toInt()?.let { + this.delay = it + } ?: run { + this.clearDelay() + } + } } \ No newline at end of file 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 00064662..ff2542a4 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 @@ -1,14 +1,14 @@ package org.mtransit.android.commons.provider.status -import com.google.transit.realtime.arrivalOrNull -import com.google.transit.realtime.departureOrNull import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.provider.GTFSRealTimeProvider +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optArrival import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDeparture import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant @@ -40,40 +40,45 @@ fun GTFSRealTimeProvider.wip( .filter { rds -> tripTargetUuidSchedule.contains(rds.uuid) } .takeIf { it.isNotEmpty() } ?: return@forEach - wipTripUpdate(tripId, gTripUpdate, tripSortedRDS, tripTargetUuidSchedule) + wipTripUpdate( + tripId, gTripUpdate, tripSortedRDS, tripTargetUuidSchedule, + isSameStop = { stu, rds -> isSameStop(stu, rds) }, + ) } } -private fun GTFSRealTimeProvider.wipTripUpdate( +internal fun wipTripUpdate( tripId: String, gTripUpdate: GTripUpdate, tripSortedRDS: List, - tripTargetUuidSchedule: Map + tripTargetUuidSchedule: Map, + isSameStop: (GTUStopTimeUpdate?, RouteDirectionStop?) -> Boolean, ) { var currentDelay = gTripUpdate.optDelay?.seconds // initial delay valid until 1st stop time update val gStopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } var stuIdx = 0 - var currentStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) - var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx + 1) + var currentStopTimeUpdate: GTUStopTimeUpdate? = null + var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) var rdsTripTimestamp: Schedule.Timestamp? = null var rdsIdx = 0 var currentRDS: RouteDirectionStop = tripSortedRDS.getOrNull(rdsIdx) ?: return // no more stop - // ### Iterate on initial stops before 1st stop time update - while (!isSameStop(currentStopTimeUpdate, currentRDS) - && rdsIdx <= tripSortedRDS.size // allow null currentRDS to signify end of trip - ) { + while (rdsIdx <= tripSortedRDS.size) { + while (!isSameStop(nextStopTimeUpdate, currentRDS) + && rdsIdx <= tripSortedRDS.size // allow null currentRDS to signify end of trip + ) { + rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } + currentDelay = wipApplyDelay(rdsTripTimestamp, currentDelay) + currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break + } + if (rdsIdx >= tripSortedRDS.size) break // no more stop + currentStopTimeUpdate = nextStopTimeUpdate ?: break // no more stop time update + nextStopTimeUpdate = gStopTimeUpdates?.getOrNull(++stuIdx) rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } - currentDelay = wipApplyDelay(rdsTripTimestamp, currentDelay) + currentDelay = wipApplyDelaySTU(rdsTripTimestamp, currentStopTimeUpdate, currentDelay) currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break } - if (rdsIdx >= tripSortedRDS.size) return // no more stop - currentStopTimeUpdate ?: return // no more stop time update - // ### use stop time update - rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } - currentDelay = wipApplyDelaySTU(rdsTripTimestamp, currentStopTimeUpdate, currentDelay) - TODO() } internal fun wipApplyDelaySTU( @@ -85,10 +90,10 @@ internal fun wipApplyDelaySTU( val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO - val stuArrivalDelay = currentStopTimeUpdate.arrivalOrNull + val stuArrivalDelay = currentStopTimeUpdate.optArrival .wipMakeDelay(timestampOriginalArrival) ?: currentDelay - val stuDepartureDelay = currentStopTimeUpdate.departureOrNull + val stuDepartureDelay = currentStopTimeUpdate.optDeparture .wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } 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 05d93b71..fe711304 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 @@ -2,10 +2,19 @@ package org.mtransit.android.commons.provider.status import com.google.transit.realtime.TripUpdateKt.stopTimeEvent import com.google.transit.realtime.TripUpdateKt.stopTimeUpdate +import com.google.transit.realtime.tripDescriptor +import com.google.transit.realtime.tripUpdate +import org.mtransit.android.commons.data.Accessibility +import org.mtransit.android.commons.data.Direction +import org.mtransit.android.commons.data.Route +import org.mtransit.android.commons.data.RouteDirectionStop +import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.data.Stop import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.data.toScheduleTimestamp +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.delayDuration import org.mtransit.android.commons.secsToInstant import org.mtransit.android.commons.toSecs import kotlin.test.Test @@ -17,7 +26,10 @@ import kotlin.test.assertTrue import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUScheduleRelationship class GTFSRealTimeTripUpdatesProviderTests { @@ -25,6 +37,8 @@ class GTFSRealTimeTripUpdatesProviderTests { private const val LOCAL_TZ_ID: String = "America/Montreal" private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: + + private const val NOW_IN_MS = 123456789_000L } // region applyDelay @@ -254,4 +268,217 @@ class GTFSRealTimeTripUpdatesProviderTests { } // endregion + + // region trip update + + private val isSameStopId: ((GTUStopTimeUpdate?, RouteDirectionStop?) -> Boolean) = + { stu, rds -> + rds?.stop?.originalIdHashString == stu?.stopId?.hashCode()?.toString() + } + + @Test + fun test_wipTripUpdate_singleTUDelay() { + val tripId = "123456789" + val tripStart = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + this.tripId = tripId + } + delayDuration = 1.minutes + } + 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(tripStart, tripId)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes, tripId)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes, tripId)))) } + } + + wipTripUpdate(tripId, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(tripStart + 1.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(tripStart + 11.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(tripStart + 21.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + } + + @Test + fun test_wipTripUpdate_2() { + val tripId = "123456789" + val startsAt = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + this.tripId = tripId + } + delayDuration = 1.minutes + stopTimeUpdate += stopTimeUpdate { + stopId = "2000" + departure = stopTimeEvent { + delayDuration = 3.minutes + } + } + stopTimeUpdate += stopTimeUpdate { + stopId = "4000" + arrival = stopTimeEvent { + delayDuration = 5.minutes + } + } + if (true) return@tripUpdate // WIP + stopTimeUpdate += stopTimeUpdate { + stopId = "7000" + scheduleRelationship = GTUScheduleRelationship.NO_DATA + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + add(makeRDS(stopId = 4000)) + add(makeRDS(stopId = 5000)) + add(makeRDS(stopId = 6000)) + if (true) return@buildList // WIP + add(makeRDS(stopId = 7000)) + add(makeRDS(stopId = 8000)) + } + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId)))) } + if (true) return@buildMap // WIP + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId)))) } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId)))) } + } + + wipTripUpdate(tripId, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 1.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 10.minutes + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 10.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 20.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[3].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[4].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 37.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 43.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[5].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 50.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + if (true) return // WIP + assertNotNull(tripTargetUuidSchedule[rdsList[6].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 60.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[7].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 70.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + } + + // end region + + @Suppress("SameParameterValue") + private fun mkTime(time: Instant, tripId: String?, arrival: Instant? = null) = + time.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + .apply { + this.tripId = tripId + } + + private fun mkSchedule( + targetUuid: String, + timestamps: List = emptyList(), + nowInMs: Long = NOW_IN_MS, + ) = Schedule( + null, + targetUuid, + nowInMs, + nowInMs, + nowInMs, + 10.seconds.inWholeMilliseconds, + false, + null, + false + ).apply { + setTimestampsAndSort(timestamps) + } + + private fun makeRDS(stopId: Int = 1) = RouteDirectionStop( + 1, + Route( + "authority", + 1, + "1", + "route 1", + "color" + ), + Direction( + "authority", + 1, + 1, + "headsign", + 1 + ), + Stop( + stopId, + "#$stopId", + "Stop #$stopId", + 1.0, + 2.0, + Accessibility.DEFAULT, + "$stopId".hashCode() + ), + false, + false, + ) } From bc070d86c0be6ca4740c5d23dc5944763e741151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 08:27:22 -0400 Subject: [PATCH 11/39] WIP --- .../status/GTFSRealTimeTripUpdatesProviderExt.kt | 12 ++++++++---- .../status/GTFSRealTimeTripUpdatesProviderTests.kt | 4 ---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 ff2542a4..4376cff3 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 @@ -22,7 +22,7 @@ import com.google.transit.realtime.GtfsRealtime.TripDescriptor as GTripDescripto import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate - +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUSTUScheduleRelationship fun GTFSRealTimeProvider.wip( rdTripUpdates: List>, @@ -83,21 +83,25 @@ internal fun wipTripUpdate( internal fun wipApplyDelaySTU( rdsTripTimestamp: Schedule.Timestamp?, - currentStopTimeUpdate: GTUStopTimeUpdate, + gStopTimeUpdate: GTUStopTimeUpdate, currentDelay: Duration? = null, ): Duration? { rdsTripTimestamp ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO - val stuArrivalDelay = currentStopTimeUpdate.optArrival + val stuArrivalDelay = gStopTimeUpdate.optArrival + .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .wipMakeDelay(timestampOriginalArrival) ?: currentDelay - val stuDepartureDelay = currentStopTimeUpdate.optDeparture + .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } + val stuDepartureDelay = gStopTimeUpdate.optDeparture + .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } return stuDepartureDelay + .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } } internal fun GTUStopTimeEvent?.wipMakeDelay( 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 fe711304..0cb5c5de 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 @@ -340,7 +340,6 @@ class GTFSRealTimeTripUpdatesProviderTests { delayDuration = 5.minutes } } - if (true) return@tripUpdate // WIP stopTimeUpdate += stopTimeUpdate { stopId = "7000" scheduleRelationship = GTUScheduleRelationship.NO_DATA @@ -353,7 +352,6 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 4000)) add(makeRDS(stopId = 5000)) add(makeRDS(stopId = 6000)) - if (true) return@buildList // WIP add(makeRDS(stopId = 7000)) add(makeRDS(stopId = 8000)) } @@ -364,7 +362,6 @@ class GTFSRealTimeTripUpdatesProviderTests { rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId)))) } rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, arrival = startsAt + 37.minutes)))) } rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId)))) } - if (true) return@buildMap // WIP rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId)))) } rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId)))) } } @@ -411,7 +408,6 @@ class GTFSRealTimeTripUpdatesProviderTests { assertTrue { timestamp.isRealTime } } } - if (true) return // WIP assertNotNull(tripTargetUuidSchedule[rdsList[6].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> assertEquals(startsAt + 60.minutes, timestamp.departure) From 0ae3b83d7e84dce90e64a6aa6e839003d7bc54c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 08:51:10 -0400 Subject: [PATCH 12/39] wip --- .../android/commons/data/Schedule.java | 4 + .../android/commons/data/ScheduleExt.kt | 3 +- .../GTFSRealTimeTripUpdatesProviderExt.kt | 24 +++-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 90 ++++++++++++------- 4 files changed, 77 insertions(+), 44 deletions(-) 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 81d2eb24..83508bd4 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -237,6 +237,10 @@ public void setTimestampsAndSort(@NonNull List timestamps) { sortTimestamps(); } + public boolean removeTimestamp(@NonNull Timestamp timestamp) { + return this.timestamps.remove(timestamp); + } + public void sortTimestamps() { CollectionUtils.sort(this.timestamps, TIMESTAMPS_COMPARATOR); resetUsefulUntilInMs(); diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index e949c01c..84b13d95 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -6,9 +6,10 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant -fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null): Schedule.Timestamp { +fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null): Schedule.Timestamp { return Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { arrival?.let { this.arrival = it } + tripId?.let { this.tripId = it } } } 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 4376cff3..8b0103fe 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 @@ -60,7 +60,6 @@ internal fun wipTripUpdate( var stuIdx = 0 var currentStopTimeUpdate: GTUStopTimeUpdate? = null var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) - var rdsTripTimestamp: Schedule.Timestamp? = null var rdsIdx = 0 var currentRDS: RouteDirectionStop = tripSortedRDS.getOrNull(rdsIdx) ?: return // no more stop @@ -68,25 +67,26 @@ internal fun wipTripUpdate( while (!isSameStop(nextStopTimeUpdate, currentRDS) && rdsIdx <= tripSortedRDS.size // allow null currentRDS to signify end of trip ) { - rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } - currentDelay = wipApplyDelay(rdsTripTimestamp, currentDelay) + currentDelay = wipApplyDelay(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentDelay) currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break } if (rdsIdx >= tripSortedRDS.size) break // no more stop currentStopTimeUpdate = nextStopTimeUpdate ?: break // no more stop time update nextStopTimeUpdate = gStopTimeUpdates?.getOrNull(++stuIdx) - rdsTripTimestamp = tripTargetUuidSchedule[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == tripId } - currentDelay = wipApplyDelaySTU(rdsTripTimestamp, currentStopTimeUpdate, currentDelay) + currentDelay = wipApplyDelaySTU(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break } } internal fun wipApplyDelaySTU( - rdsTripTimestamp: Schedule.Timestamp?, + tripId: String, + rdsSchedule: Schedule?, gStopTimeUpdate: GTUStopTimeUpdate, currentDelay: Duration? = null, ): Duration? { - rdsTripTimestamp ?: return null // impossible to handle + val rdsTripTimestamp = rdsSchedule?.timestamps + ?.singleOrNull { it.tripId == tripId } // TODO handle multiple stops at this stop during same trip + ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO @@ -100,6 +100,9 @@ internal fun wipApplyDelaySTU( .wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } + if (gStopTimeUpdate.scheduleRelationship == GTUSTUScheduleRelationship.SKIPPED) { + rdsSchedule.removeTimestamp(rdsTripTimestamp) + } return stuDepartureDelay .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } } @@ -119,11 +122,14 @@ internal fun GTUStopTimeEvent?.wipMakeDelay( } internal fun wipApplyDelay( - rdsTripTimestamp: Schedule.Timestamp?, + tripId: String, + rdsSchedule: Schedule?, currentDelay: Duration? ): Duration? { currentDelay ?: return null - rdsTripTimestamp ?: return currentDelay + val rdsTripTimestamp = rdsSchedule?.timestamps + ?.singleOrNull { it.tripId == tripId } // TODO handle multiple stops at this stop during same trip + ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.departure - rdsTripTimestamp.arrival if (currentDelay < Duration.ZERO) { rdsTripTimestamp.arrival += currentDelay 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 0cb5c5de..df5f7f68 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 @@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent -import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUScheduleRelationship +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUSTUScheduleRelationship class GTFSRealTimeTripUpdatesProviderTests { @@ -39,6 +39,8 @@ class GTFSRealTimeTripUpdatesProviderTests { private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: private const val NOW_IN_MS = 123456789_000L + + private const val TRIP_ID = "123456789" } // region applyDelay @@ -46,10 +48,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_null() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) val delay: Duration? = null - val result = wipApplyDelay(timestamp, delay) + val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) assertNull(result) assertFalse { timestamp.isRealTime } @@ -59,10 +61,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_0_on_time() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) val delay = Duration.ZERO - val result = wipApplyDelay(timestamp, delay) + val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -73,10 +75,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_simple_late() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) val delay = 10.minutes - val result = wipApplyDelay(timestamp, delay) + val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -88,10 +90,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_differentArrival_late() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val delay = 10.minutes - val result = wipApplyDelay(timestamp, delay) + val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(9.minutes, result) // delay partially consumed @@ -104,10 +106,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_consumed_late() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 15.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val delay = 10.minutes - val result = wipApplyDelay(timestamp, delay) + val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(Duration.ZERO, result) // delay consumed @@ -120,10 +122,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_simple_early() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val delay = (-5).minutes - val result = wipApplyDelay(timestamp, delay) + val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -139,14 +141,14 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelaySTU_simple() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { time = (departure + 1.minutes).toSecs() } } - val result = wipApplyDelaySTU(timestamp, stopTimeUpdate) + val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(1.minutes, result) @@ -158,7 +160,7 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelaySTU_2() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val stopTimeUpdate = stopTimeUpdate { this.arrival = stopTimeEvent { time = (arrival - 1.minutes).toSecs() @@ -168,7 +170,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(timestamp, stopTimeUpdate) + val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(2.minutes, result) @@ -182,14 +184,14 @@ class GTFSRealTimeTripUpdatesProviderTests { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes val delay = 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { time = (departure + 2.minutes).toSecs() } } - val result = wipApplyDelaySTU(timestamp, stopTimeUpdate, delay) + val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -203,14 +205,14 @@ class GTFSRealTimeTripUpdatesProviderTests { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val delay = 15.minutes // should be ignored - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val stopTimeUpdate = stopTimeUpdate { this.arrival = stopTimeEvent { time = (arrival + 3.minutes).toSecs() } } - val result = wipApplyDelaySTU(timestamp, stopTimeUpdate, delay) + val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -253,7 +255,7 @@ class GTFSRealTimeTripUpdatesProviderTests { fun test_makeDelay_3() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) val previousDelay = 10.minutes val stopTimeEvent: GTUStopTimeEvent? = null @@ -278,7 +280,6 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_singleTUDelay() { - val tripId = "123456789" val tripStart = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -292,12 +293,12 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes, tripId)))) } + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart, TRIP_ID)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes, TRIP_ID)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes, TRIP_ID)))) } } - wipTripUpdate(tripId, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -342,7 +343,11 @@ class GTFSRealTimeTripUpdatesProviderTests { } stopTimeUpdate += stopTimeUpdate { stopId = "7000" - scheduleRelationship = GTUScheduleRelationship.NO_DATA + scheduleRelationship = GTUSTUScheduleRelationship.SKIPPED + } + stopTimeUpdate += stopTimeUpdate { + stopId = "9000" + scheduleRelationship = GTUSTUScheduleRelationship.NO_DATA } } val rdsList = buildList { @@ -354,6 +359,8 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 6000)) add(makeRDS(stopId = 7000)) add(makeRDS(stopId = 8000)) + add(makeRDS(stopId = 9000)) + add(makeRDS(stopId = 10000)) } val tripTargetUuidSchedule = buildMap { rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } @@ -364,9 +371,11 @@ class GTFSRealTimeTripUpdatesProviderTests { rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId)))) } rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId)))) } rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId)))) } + rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId)))) } } - wipTripUpdate(tripId, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -409,14 +418,23 @@ class GTFSRealTimeTripUpdatesProviderTests { } } assertNotNull(tripTargetUuidSchedule[rdsList[6].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[7].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> - assertEquals(startsAt + 60.minutes, timestamp.departure) + assertEquals(startsAt + 70.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[8].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 80.minutes, timestamp.departure) assertFalse { timestamp.isRealTime } } } - assertNotNull(tripTargetUuidSchedule[rdsList[7].uuid]) { schedule -> + assertNotNull(tripTargetUuidSchedule[rdsList[9].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> - assertEquals(startsAt + 70.minutes, timestamp.departure) + assertEquals(startsAt + 90.minutes, timestamp.departure) assertFalse { timestamp.isRealTime } } } @@ -424,15 +442,19 @@ class GTFSRealTimeTripUpdatesProviderTests { // end region + private fun Schedule.Timestamp.toSchedule() = mkSchedule( + timestamps = listOf(this), + ) + @Suppress("SameParameterValue") private fun mkTime(time: Instant, tripId: String?, arrival: Instant? = null) = - time.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + time.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) .apply { this.tripId = tripId } private fun mkSchedule( - targetUuid: String, + targetUuid: String = makeRDS().uuid, timestamps: List = emptyList(), nowInMs: Long = NOW_IN_MS, ) = Schedule( From 27263875bc01efa9297531daac080bf8103423c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 08:53:39 -0400 Subject: [PATCH 13/39] wip --- .../provider/status/GTFSRealTimeTripUpdatesProviderTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 df5f7f68..8ca8ec6b 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 @@ -338,7 +338,7 @@ class GTFSRealTimeTripUpdatesProviderTests { stopTimeUpdate += stopTimeUpdate { stopId = "4000" arrival = stopTimeEvent { - delayDuration = 5.minutes + time = ((startsAt + 30.minutes) + 5.minutes).toSecs() } } stopTimeUpdate += stopTimeUpdate { From 44eb926a9873260e7389e68b21fee6e8d9a82c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 09:06:23 -0400 Subject: [PATCH 14/39] wip --- .../GTFSRealTimeTripUpdatesProviderExt.kt | 18 ++++- .../GTFSRealTimeTripUpdatesProviderTests.kt | 73 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) 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 8b0103fe..27bed777 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 @@ -9,9 +9,11 @@ import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optArrival import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDeparture +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId import org.mtransit.android.commons.provider.gtfs.parseStopId import org.mtransit.android.commons.provider.gtfs.parseTripId @@ -19,6 +21,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant import com.google.transit.realtime.GtfsRealtime.TripDescriptor as GTripDescriptor +import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate @@ -54,13 +57,22 @@ internal fun wipTripUpdate( tripTargetUuidSchedule: Map, isSameStop: (GTUStopTimeUpdate?, RouteDirectionStop?) -> Boolean, ) { + if (gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.CANCELED + || gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.DELETED + ) { + tripTargetUuidSchedule.values.forEach { schedule -> + schedule ?: return@forEach + schedule.timestamps.filter { it.tripId == tripId }.forEach { + schedule.removeTimestamp(it) + } + } + } + var stuIdx = 0 + var rdsIdx = 0 var currentDelay = gTripUpdate.optDelay?.seconds // initial delay valid until 1st stop time update val gStopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } - - var stuIdx = 0 var currentStopTimeUpdate: GTUStopTimeUpdate? = null var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) - var rdsIdx = 0 var currentRDS: RouteDirectionStop = tripSortedRDS.getOrNull(rdsIdx) ?: return // no more stop while (rdsIdx <= tripSortedRDS.size) { 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 8ca8ec6b..ccce2657 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 @@ -27,6 +27,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant +import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUSTUScheduleRelationship @@ -321,7 +322,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_2() { + fun test_wipTripUpdate_combined_complex() { val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { @@ -440,6 +441,76 @@ class GTFSRealTimeTripUpdatesProviderTests { } } + @Test + fun test_wipTripUpdate_trip_cancelled() { + val tripId = "123456789" + val startsAt = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + this.tripId = tripId + this.scheduleRelationship = GTDScheduleRelationship.CANCELED + } + delayDuration = 1.minutes + } + 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, tripId)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + } + + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + } + + @Test + fun test_wipTripUpdate_trip_deleted() { + val tripId = "123456789" + val startsAt = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + this.tripId = tripId + this.scheduleRelationship = GTDScheduleRelationship.DELETED + } + delayDuration = 1.minutes + } + 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, tripId)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + } + + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + } + // end region private fun Schedule.Timestamp.toSchedule() = mkSchedule( From 43624e4c859f4c2f6b834e53de9ea76e58c7ae2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 13:44:26 -0400 Subject: [PATCH 15/39] wip --- .../GTFSRealTimeTripUpdatesProviderExt.kt | 72 +++++-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 184 +++++++++++++++++- 2 files changed, 228 insertions(+), 28 deletions(-) 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 27bed777..909d4d9e 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 @@ -10,6 +10,7 @@ import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optArrival import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDeparture import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopId import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant @@ -43,19 +44,46 @@ fun GTFSRealTimeProvider.wip( .filter { rds -> tripTargetUuidSchedule.contains(rds.uuid) } .takeIf { it.isNotEmpty() } ?: return@forEach + val sortedTargetUuidAndSequence = makeTargetUuidAndSequenceList(tripId, tripTargetUuidSchedule, tripSortedRDS) wipTripUpdate( - tripId, gTripUpdate, tripSortedRDS, tripTargetUuidSchedule, - isSameStop = { stu, rds -> isSameStop(stu, rds) }, + tripId, gTripUpdate, tripSortedRDS, sortedTargetUuidAndSequence, tripTargetUuidSchedule, + isSameStop = { stu, rds, stopSeq -> isSameStop(stu, rds, stopSeq) }, ) } } +internal fun makeTargetUuidAndSequenceList( + tripId: String, + tripTargetUuidSchedule: Map, + tripSortedRDS: List, +): List> { + if (tripTargetUuidSchedule.values.any { it?.timestamps?.filter { it.tripId == tripId }?.any { it.stopSequenceOrNull == null } == true }) { + // should not happen if FF is turned ON + return tripSortedRDS + .mapIndexed { index, rds -> + rds.uuid to index + 1 // generated stop sequence + } + .sortedBy { (_, stopSequence) -> stopSequence } + } + var generatedStopSequence = 1 + return buildList { + tripTargetUuidSchedule.forEach { targetUuid, schedule -> + schedule?.timestamps?.filter { it.tripId == tripId }?.forEach { timestamp -> + val stopSequence = timestamp.stopSequenceOrNull ?: generatedStopSequence + add(targetUuid to stopSequence) + generatedStopSequence = stopSequence + 1 // next probable stop sequence + } + } + }.sortedBy { (_, stopSequence) -> stopSequence } +} + internal fun wipTripUpdate( tripId: String, gTripUpdate: GTripUpdate, tripSortedRDS: List, + sortedTargetUuidAndSequence: List>, tripTargetUuidSchedule: Map, - isSameStop: (GTUStopTimeUpdate?, RouteDirectionStop?) -> Boolean, + isSameStop: (GTUStopTimeUpdate?, RouteDirectionStop, Int) -> Boolean, ) { if (gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.CANCELED || gTripUpdate.optTrip?.optScheduleRelationship == GTDScheduleRelationship.DELETED @@ -68,25 +96,29 @@ internal fun wipTripUpdate( } } var stuIdx = 0 - var rdsIdx = 0 + var uuidAndSeqIdx = 0 var currentDelay = gTripUpdate.optDelay?.seconds // initial delay valid until 1st stop time update val gStopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } var currentStopTimeUpdate: GTUStopTimeUpdate? = null var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) - var currentRDS: RouteDirectionStop = tripSortedRDS.getOrNull(rdsIdx) + var currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(uuidAndSeqIdx) ?: return // no more stop - while (rdsIdx <= tripSortedRDS.size) { - while (!isSameStop(nextStopTimeUpdate, currentRDS) - && rdsIdx <= tripSortedRDS.size // allow null currentRDS to signify end of trip + var currentRDS: RouteDirectionStop = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } + ?: return // stop not found! + while (uuidAndSeqIdx <= sortedTargetUuidAndSequence.size) { + while (!isSameStop(nextStopTimeUpdate, currentRDS, currentUuidAndSeq.second) + && uuidAndSeqIdx <= sortedTargetUuidAndSequence.size // allow null currentRDS to signify end of trip ) { currentDelay = wipApplyDelay(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentDelay) - currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break + currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop + currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } - if (rdsIdx >= tripSortedRDS.size) break // no more stop + if (uuidAndSeqIdx >= sortedTargetUuidAndSequence.size) break // no more stop currentStopTimeUpdate = nextStopTimeUpdate ?: break // no more stop time update nextStopTimeUpdate = gStopTimeUpdates?.getOrNull(++stuIdx) currentDelay = wipApplyDelaySTU(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) - currentRDS = tripSortedRDS.getOrNull(++rdsIdx) ?: break + currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop + currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } } @@ -161,15 +193,19 @@ internal fun wipApplyDelay( } } +private fun GTFSRealTimeProvider.isSameStop(stopTimeUpdate: GTUStopTimeUpdate?, rds: RouteDirectionStop?, stopSequence: Int) = + stopTimeUpdate?.isSameStop(rds, stopSequence, this::parseStopId) == true -private fun GTFSRealTimeProvider.isSameStop( - stopTimeUpdate: GTUStopTimeUpdate?, +internal fun GTUStopTimeUpdate.isSameStop( rds: RouteDirectionStop?, - @Suppress("unused") currentStopSequence: Int? = null, + stopSequence: Int, + parseStopId: (String) -> String, ): Boolean { - stopTimeUpdate ?: return false rds ?: return false - // TODO check stop sequence as well? - // TODO what about stop present multiple times in same trip? - return rds.stop.isSameOriginalId(parseStopId(stopTimeUpdate)) + val sameOrUnspecifiedStopSequence = this.optStopSequence + ?.let { it == stopSequence } + val sameOrUnspecifiedStopId = this.optStopId?.let { + rds.stop.isSameOriginalId(parseStopId(it)) + } + return sameOrUnspecifiedStopSequence == true || sameOrUnspecifiedStopId == true } 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 ccce2657..9dc482f7 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 @@ -44,6 +44,21 @@ class GTFSRealTimeTripUpdatesProviderTests { private const val TRIP_ID = "123456789" } + // region same stop + + @Test + fun test_isSameStop() { + assertTrue { stopTimeUpdate { stopId = "1234" }.isSameStop(makeRDS(stopId = 1234), 1, parseStopId = { it }) } + assertFalse { stopTimeUpdate { stopId = "1234" }.isSameStop(makeRDS(stopId = 5678), 1, parseStopId = { it }) } + assertFalse { stopTimeUpdate { }.isSameStop(makeRDS(stopId = 1234), 1, parseStopId = { it }) } + assertTrue { stopTimeUpdate { stopSequence = 7 }.isSameStop(makeRDS(stopId = 1234), 7, parseStopId = { it }) } + assertFalse { stopTimeUpdate { }.isSameStop(makeRDS(stopId = 1234), 7, parseStopId = { it }) } + assertTrue { stopTimeUpdate { stopId = "1234"; stopSequence = 7 }.isSameStop(makeRDS(stopId = 1234), 7, parseStopId = { it }) } + assertFalse { stopTimeUpdate { stopId = "1234"; stopSequence = 7 }.isSameStop(makeRDS(stopId = 5678), 1, parseStopId = { it }) } + } + + // endregion + // region applyDelay @Test @@ -274,9 +289,9 @@ class GTFSRealTimeTripUpdatesProviderTests { // region trip update - private val isSameStopId: ((GTUStopTimeUpdate?, RouteDirectionStop?) -> Boolean) = - { stu, rds -> - rds?.stop?.originalIdHashString == stu?.stopId?.hashCode()?.toString() + private val isSameStop: ((GTUStopTimeUpdate?, RouteDirectionStop?, Int) -> Boolean) = + { stu, rds, stopSeq -> + stu?.isSameStop(rds, stopSeq, parseStopId = { it } ) == true } @Test @@ -298,8 +313,13 @@ class GTFSRealTimeTripUpdatesProviderTests { rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes, TRIP_ID)))) } rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes, TRIP_ID)))) } } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -322,7 +342,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_combined_complex() { + fun test_wipTripUpdate_combined_complex_stop_id() { val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { @@ -375,8 +395,141 @@ class GTFSRealTimeTripUpdatesProviderTests { rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId)))) } rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId)))) } } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } + + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 1.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 10.minutes + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 10.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 20.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[3].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[4].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 37.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 43.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[5].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 50.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[6].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[7].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 70.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[8].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 80.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[9].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 90.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + } + + @Test + fun test_wipTripUpdate_combined_complex_stop_sequence() { + val tripId = "123456789" + val startsAt = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + this.tripId = tripId + } + delayDuration = 1.minutes + stopTimeUpdate += stopTimeUpdate { + stopSequence = 2 + departure = stopTimeEvent { + delayDuration = 3.minutes + } + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 4 + arrival = stopTimeEvent { + time = ((startsAt + 30.minutes) + 5.minutes).toSecs() + } + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 7 + scheduleRelationship = GTUSTUScheduleRelationship.SKIPPED + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 9 + scheduleRelationship = GTUSTUScheduleRelationship.NO_DATA + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + add(makeRDS(stopId = 4000)) + add(makeRDS(stopId = 5000)) + add(makeRDS(stopId = 6000)) + add(makeRDS(stopId = 7000)) + add(makeRDS(stopId = 8000)) + add(makeRDS(stopId = 9000)) + add(makeRDS(stopId = 10000)) + } + var stopSeq = 0 + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId, ++stopSeq)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId, ++stopSeq)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId, ++stopSeq)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId, ++stopSeq)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, ++stopSeq, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId, ++stopSeq)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId, ++stopSeq)))) } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId, ++stopSeq)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId, ++stopSeq)))) } + rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId, ++stopSeq)))) } + } + val sortedTargetUuidAndSequence = buildList { + tripTargetUuidSchedule.forEach { (uuid, schedule) -> + schedule?.timestamps?.forEach { timestamp -> + add(uuid to assertNotNull(timestamp.stopSequenceOrNull)) + } + } + } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -462,8 +615,13 @@ class GTFSRealTimeTripUpdatesProviderTests { rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertTrue { schedule.timestamps.isEmpty() } @@ -497,8 +655,13 @@ class GTFSRealTimeTripUpdatesProviderTests { rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } } + val sortedTargetUuidAndSequence = buildList { + rdsList.forEachIndexed { index, rds -> + add(rds.uuid to index + 1) + } + } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, tripTargetUuidSchedule, isSameStopId) + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertTrue { schedule.timestamps.isEmpty() } @@ -518,10 +681,11 @@ class GTFSRealTimeTripUpdatesProviderTests { ) @Suppress("SameParameterValue") - private fun mkTime(time: Instant, tripId: String?, arrival: Instant? = null) = + private fun mkTime(time: Instant, tripId: String?, stopSequence: Int? = null, arrival: Instant? = null) = time.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) .apply { this.tripId = tripId + stopSequence?.let { this.setStopSequence(it) } } private fun mkSchedule( @@ -565,7 +729,7 @@ class GTFSRealTimeTripUpdatesProviderTests { 1.0, 2.0, Accessibility.DEFAULT, - "$stopId".hashCode() + stopId, // "$stopId".hashCode() ), false, false, From e3bc33b9f1450d869060448e68566f5f8501974f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 14:11:18 -0400 Subject: [PATCH 16/39] Cleanup discouraged Schedule.Timestamp --- .../android/commons/data/Schedule.java | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) 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 6aa701ef..deb64d2b 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -432,8 +432,7 @@ public String getLogTag() { return LOG_TAG; } - @Discouraged(message = "use getDepartureT()/setDepartureT") - public long t; // final + private long departureInMs; @Direction.HeadSignType private int headsignType = Direction.HEADSIGN_TYPE_NONE; @Nullable @@ -454,8 +453,7 @@ public String getLogTag() { @VisibleForTesting public Timestamp(long departureT) { - //noinspection DiscouragedApi - this.t = departureT; + this.departureInMs = departureT; } public Timestamp(long departureT, @NonNull TimeZone localTimeZone) { @@ -463,25 +461,17 @@ public Timestamp(long departureT, @NonNull TimeZone localTimeZone) { } public Timestamp(long departureT, @NonNull String localTimeZoneId) { - //noinspection DiscouragedApi - this.t = departureT; + this.departureInMs = departureT; this.localTimeZoneId = localTimeZoneId; } - @Discouraged(message = "use getDepartureT()") - public long getT() { - return getDepartureT(); - } - public long getDepartureT() { - //noinspection DiscouragedApi - return this.t; + return this.departureInMs; } public void setDepartureT(long departureT) { final long originalArrivalT = getArrivalT(); // stored as diff -> do not change - //noinspection DiscouragedApi - this.t = departureT; + this.departureInMs = departureT; setArrivalT(originalArrivalT); // stored as diff -> do not change } @@ -498,11 +488,6 @@ public void setArrivalT(long arrivalT) { setArrivalDiffMs(getDepartureT() - arrivalT); } - @Discouraged(message = "use setArrivalT()") - public void setArrivalTimestamp(long arrivalTimestamp) { - setArrivalDiffMs(getDepartureT() - arrivalTimestamp); - } - public void setArrivalDiffMs(@Nullable Long arrivalDiffMs) { this.arrivalDiffMs = arrivalDiffMs; } @@ -685,8 +670,7 @@ public boolean equals(Object o) { Timestamp timestamp = (Timestamp) o; - //noinspection DiscouragedApi - if (t != timestamp.t) return false; + if (departureInMs != timestamp.departureInMs) return false; if (headsignType != timestamp.headsignType) return false; if (!Objects.equals(headsignValue, timestamp.headsignValue)) return false; if (!Objects.equals(localTimeZoneId, timestamp.localTimeZoneId)) return false; @@ -702,8 +686,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - //noinspection DiscouragedApi - int result = Long.hashCode(t); + int result = Long.hashCode(departureInMs); result = 31 * result + headsignType; result = 31 * result + (headsignValue != null ? headsignValue.hashCode() : 0); result = 31 * result + (localTimeZoneId != null ? localTimeZoneId.hashCode() : 0); @@ -754,7 +737,7 @@ public String toString() { return sb.toString(); } - private static final String JSON_TIMESTAMP = "t"; + private static final String JSON_DEPARTURE = "t"; private static final String JSON_ARRIVAL_DIFF = "tDiffA"; private static final String JSON_TRIP_ID = "trip_id"; private static final String JSON_STOP_SEQUENCE = "stop_seq"; @@ -768,8 +751,8 @@ public String toString() { @Nullable static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { try { - final long t = jTimestamp.getLong(JSON_TIMESTAMP); - final Timestamp timestamp = new Timestamp(t); + final long departureInMs = jTimestamp.getLong(JSON_DEPARTURE); + final Timestamp timestamp = new Timestamp(departureInMs); if (jTimestamp.has(JSON_ARRIVAL_DIFF)) { timestamp.setArrivalDiffMs(jTimestamp.getLong(JSON_ARRIVAL_DIFF)); } @@ -817,8 +800,7 @@ public JSONObject toJSON() { public static JSONObject toJSON(@NonNull Timestamp timestamp) { try { JSONObject jTimestamp = new JSONObject(); - //noinspection DiscouragedApi - jTimestamp.put(JSON_TIMESTAMP, timestamp.t); + jTimestamp.put(JSON_DEPARTURE, timestamp.departureInMs); if (timestamp.arrivalDiffMs != null) { jTimestamp.put(JSON_ARRIVAL_DIFF, timestamp.arrivalDiffMs); } From 363d0ec64ef499b50464d4a4f8f237064f3ef116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Mon, 9 Mar 2026 14:19:06 -0400 Subject: [PATCH 17/39] cleanup --- .../commons/provider/gtfs/GTFSScheduleTimestampsProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java index 4d0c7593..0657a8a7 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java @@ -104,7 +104,8 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro } dataRequests++; // 1 more data request done for (Schedule.Timestamp t : dayTimestamps) { - if (t.getDepartureT() >= startsAtInMs && t.getDepartureT() < endsAtInMs) { + final long departureT = t.getDepartureT(); + if (startsAtInMs <= departureT && departureT < endsAtInMs) { allTimestamps.add(t); } } From ee668effed59ab22c8e3372619eb387568d0b512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 10 Mar 2026 14:31:07 -0400 Subject: [PATCH 18/39] post-merge --- src/main/java/org/mtransit/android/commons/data/Schedule.java | 4 ---- 1 file changed, 4 deletions(-) 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 bd361a0b..deb64d2b 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -475,10 +475,6 @@ public void setDepartureT(long departureT) { setArrivalT(originalArrivalT); // stored as diff -> do not change } - public long getDepartureT() { - return t; - } - public long getArrivalT() { return getDepartureT() - (arrivalDiffMs == null ? 0L : arrivalDiffMs); } From 638cbea9a976755aa2d598c7283bd4bb7e24f290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 10 Mar 2026 15:03:37 -0400 Subject: [PATCH 19/39] cleanup --- .../provider/status/GTFSRealTimeTripUpdatesProviderExt.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 909d4d9e..7304770c 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 @@ -57,7 +57,9 @@ internal fun makeTargetUuidAndSequenceList( tripTargetUuidSchedule: Map, tripSortedRDS: List, ): List> { - if (tripTargetUuidSchedule.values.any { it?.timestamps?.filter { it.tripId == tripId }?.any { it.stopSequenceOrNull == null } == true }) { + if (tripTargetUuidSchedule.values.any { + it?.timestamps?.filter { timestamp -> timestamp.tripId == tripId }?.any { timestamp -> timestamp.stopSequenceOrNull == null } == true + }) { // should not happen if FF is turned ON return tripSortedRDS .mapIndexed { index, rds -> @@ -67,7 +69,7 @@ internal fun makeTargetUuidAndSequenceList( } var generatedStopSequence = 1 return buildList { - tripTargetUuidSchedule.forEach { targetUuid, schedule -> + tripTargetUuidSchedule.forEach { (targetUuid, schedule) -> schedule?.timestamps?.filter { it.tripId == tripId }?.forEach { timestamp -> val stopSequence = timestamp.stopSequenceOrNull ?: generatedStopSequence add(targetUuid to stopSequence) From d6caeeaf7e423559277ad37e8429124b11ee29fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 10 Mar 2026 15:42:24 -0400 Subject: [PATCH 20/39] wip --- .../android/commons/data/ScheduleExt.kt | 6 +- .../GTFSRealTimeTripUpdatesProviderExt.kt | 12 +- .../android/commons/data/ScheduleExtTests.kt | 2 +- .../GTFSRealTimeTripUpdatesProviderTests.kt | 253 +++++++++++++----- 4 files changed, 200 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 84b13d95..17f109a8 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -6,12 +6,12 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant -fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null): Schedule.Timestamp { - return Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { +fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null, stopSequence: Int? = null) = + Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { arrival?.let { this.arrival = it } tripId?.let { this.tripId = it } + stopSequence?.let { this.setStopSequence(it) } } -} var Schedule.Timestamp.departure: Instant get() = departureT.millisToInstant() 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 7304770c..50d9fa5e 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 @@ -111,14 +111,14 @@ internal fun wipTripUpdate( while (!isSameStop(nextStopTimeUpdate, currentRDS, currentUuidAndSeq.second) && uuidAndSeqIdx <= sortedTargetUuidAndSequence.size // allow null currentRDS to signify end of trip ) { - currentDelay = wipApplyDelay(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentDelay) + currentDelay = wipApplyDelay(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], 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 = wipApplyDelaySTU(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) + currentDelay = wipApplyDelaySTU(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } @@ -126,12 +126,14 @@ internal fun wipTripUpdate( internal fun wipApplyDelaySTU( tripId: String, + stopSequence: Int, rdsSchedule: Schedule?, gStopTimeUpdate: GTUStopTimeUpdate, currentDelay: Duration? = null, ): Duration? { val rdsTripTimestamp = rdsSchedule?.timestamps - ?.singleOrNull { it.tripId == tripId } // TODO handle multiple stops at this stop during same trip + ?.singleOrNull { it.tripId == tripId + && (it.stopSequenceOrNull == null || it.stopSequenceOrNull == stopSequence) } ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure @@ -169,12 +171,14 @@ internal fun GTUStopTimeEvent?.wipMakeDelay( internal fun wipApplyDelay( tripId: String, + stopSequence: Int, rdsSchedule: Schedule?, currentDelay: Duration? ): Duration? { currentDelay ?: return null val rdsTripTimestamp = rdsSchedule?.timestamps - ?.singleOrNull { it.tripId == tripId } // TODO handle multiple stops at this stop during same trip + ?.singleOrNull { it.tripId == tripId + && (it.stopSequenceOrNull == null || it.stopSequenceOrNull == stopSequence) } ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.departure - rdsTripTimestamp.arrival if (currentDelay < Duration.ZERO) { diff --git a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt index 3438c66e..6a07f28e 100644 --- a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt +++ b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt @@ -13,7 +13,7 @@ class ScheduleExtTests { } @Test - fun test1() { + fun test_departure_update_no_effect_on_arrival() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) 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 9dc482f7..6db1b82a 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 @@ -42,6 +42,7 @@ class GTFSRealTimeTripUpdatesProviderTests { private const val NOW_IN_MS = 123456789_000L private const val TRIP_ID = "123456789" + private const val STOP_SEQUENCE = 1 } // region same stop @@ -64,10 +65,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_null() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val delay: Duration? = null - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNull(result) assertFalse { timestamp.isRealTime } @@ -77,10 +78,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_0_on_time() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val delay = Duration.ZERO - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -91,10 +92,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_simple_late() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -106,10 +107,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_differentArrival_late() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(9.minutes, result) // delay partially consumed @@ -122,10 +123,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_consumed_late() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 15.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(Duration.ZERO, result) // delay consumed @@ -138,10 +139,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_simple_early() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val delay = (-5).minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -157,14 +158,14 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelaySTU_simple() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { time = (departure + 1.minutes).toSecs() } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(1.minutes, result) @@ -176,7 +177,7 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelaySTU_2() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { this.arrival = stopTimeEvent { time = (arrival - 1.minutes).toSecs() @@ -186,7 +187,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(2.minutes, result) @@ -200,14 +201,14 @@ class GTFSRealTimeTripUpdatesProviderTests { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes val delay = 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { time = (departure + 2.minutes).toSecs() } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate, delay) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -221,14 +222,14 @@ class GTFSRealTimeTripUpdatesProviderTests { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val delay = 15.minutes // should be ignored - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { this.arrival = stopTimeEvent { time = (arrival + 3.minutes).toSecs() } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate, delay) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -271,7 +272,7 @@ class GTFSRealTimeTripUpdatesProviderTests { fun test_makeDelay_3() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val previousDelay = 10.minutes val stopTimeEvent: GTUStopTimeEvent? = null @@ -291,7 +292,7 @@ class GTFSRealTimeTripUpdatesProviderTests { private val isSameStop: ((GTUStopTimeUpdate?, RouteDirectionStop?, Int) -> Boolean) = { stu, rds, stopSeq -> - stu?.isSameStop(rds, stopSeq, parseStopId = { it } ) == true + stu?.isSameStop(rds, stopSeq, parseStopId = { it }) == true } @Test @@ -309,9 +310,9 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart, TRIP_ID)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes, TRIP_ID)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes, TRIP_ID)))) } + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes)))) } } val sortedTargetUuidAndSequence = buildList { rdsList.forEachIndexed { index, rds -> @@ -343,11 +344,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_combined_complex_stop_id() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID } delayDuration = 1.minutes stopTimeUpdate += stopTimeUpdate { @@ -384,16 +384,16 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 10000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } - rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId)))) } - rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, arrival = startsAt + 37.minutes)))) } - rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId)))) } - rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId)))) } - rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId)))) } - rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId)))) } - rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId)))) } + 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)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes)))) } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes)))) } + rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes)))) } } val sortedTargetUuidAndSequence = buildList { rdsList.forEachIndexed { index, rds -> @@ -468,11 +468,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_combined_complex_stop_sequence() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID } delayDuration = 1.minutes stopTimeUpdate += stopTimeUpdate { @@ -510,16 +509,16 @@ class GTFSRealTimeTripUpdatesProviderTests { } var stopSeq = 0 val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId, ++stopSeq)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId, ++stopSeq)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId, ++stopSeq)))) } - rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId, ++stopSeq)))) } - rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, ++stopSeq, arrival = startsAt + 37.minutes)))) } - rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId, ++stopSeq)))) } - rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId, ++stopSeq)))) } - rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId, ++stopSeq)))) } - rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId, ++stopSeq)))) } - rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId, ++stopSeq)))) } + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, stopSeq = ++stopSeq)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, stopSeq = ++stopSeq)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, stopSeq = ++stopSeq)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, stopSeq = ++stopSeq)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, stopSeq = ++stopSeq, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, stopSeq = ++stopSeq)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, stopSeq = ++stopSeq)))) } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, stopSeq = ++stopSeq)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, stopSeq = ++stopSeq)))) } + rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, stopSeq = ++stopSeq)))) } } val sortedTargetUuidAndSequence = buildList { tripTargetUuidSchedule.forEach { (uuid, schedule) -> @@ -594,13 +593,142 @@ class GTFSRealTimeTripUpdatesProviderTests { } } + @Test + fun test_wipTripUpdate_combined_complex_stop_sequence_repeated_stop() { + val startsAt = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + } + delayDuration = 1.minutes + stopTimeUpdate += stopTimeUpdate { + stopSequence = 2 + departure = stopTimeEvent { + delayDuration = 3.minutes + } + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 4 + arrival = stopTimeEvent { + time = ((startsAt + 30.minutes) + 5.minutes).toSecs() + } + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 7 + scheduleRelationship = GTUSTUScheduleRelationship.SKIPPED + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 9 + scheduleRelationship = GTUSTUScheduleRelationship.NO_DATA + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + add(makeRDS(stopId = 4000)) + add(makeRDS(stopId = 5000)) + add(makeRDS(stopId = 6000)) + add(makeRDS(stopId = 7000)) + add(makeRDS(stopId = 9000)) + add(makeRDS(stopId = 10000)) + } + var stopSeq = 0 + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, stopSeq = ++stopSeq)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, stopSeq = ++stopSeq)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, stopSeq = ++stopSeq)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, stopSeq = ++stopSeq)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, stopSeq = ++stopSeq, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, stopSeq = ++stopSeq)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, stopSeq = ++stopSeq)))) } + get(rdsList[3].uuid)?.apply { + addTimestampWithoutSort(mkTime(startsAt + 70.minutes, stopSeq = ++stopSeq)) + sortTimestamps() + } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, stopSeq = ++stopSeq)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, stopSeq = ++stopSeq)))) } + } + val sortedTargetUuidAndSequence = buildList { + tripTargetUuidSchedule.forEach { (uuid, schedule) -> + schedule?.timestamps?.forEach { timestamp -> + add(uuid to assertNotNull(timestamp.stopSequenceOrNull)) + } + } + }.sortedBy { (_, stopSequence) -> stopSequence } + + + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 1.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 10.minutes + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 10.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 20.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[3].uuid]) { schedule -> + assertNotNull(schedule.timestamps.getOrNull(0)) { timestamp -> + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[4].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 37.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 43.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[5].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 50.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[6].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[3].uuid]) { schedule -> + assertNotNull(schedule.timestamps.getOrNull(1)) { timestamp -> + assertEquals(startsAt + 70.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[7].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 80.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[8].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 90.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + } + @Test fun test_wipTripUpdate_trip_cancelled() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID this.scheduleRelationship = GTDScheduleRelationship.CANCELED } delayDuration = 1.minutes @@ -611,9 +739,9 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + 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 -> @@ -636,11 +764,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_trip_deleted() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID this.scheduleRelationship = GTDScheduleRelationship.DELETED } delayDuration = 1.minutes @@ -651,9 +778,9 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + 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 -> @@ -681,12 +808,8 @@ class GTFSRealTimeTripUpdatesProviderTests { ) @Suppress("SameParameterValue") - private fun mkTime(time: Instant, tripId: String?, stopSequence: Int? = null, arrival: Instant? = null) = - time.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) - .apply { - this.tripId = tripId - stopSequence?.let { this.setStopSequence(it) } - } + private fun mkTime(time: Instant, tripId: String? = TRIP_ID, stopSeq: Int? = null, arrival: Instant? = null) = + time.toScheduleTimestamp(LOCAL_TZ_ID, arrival, tripId, stopSeq) private fun mkSchedule( targetUuid: String = makeRDS().uuid, From 6f8352885ddc952ecb0ef51f140ec0ea0ab50950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 10 Mar 2026 15:42:24 -0400 Subject: [PATCH 21/39] wip --- .../android/commons/data/POIStatus.java | 12 +- .../android/commons/data/Schedule.java | 7 +- .../android/commons/data/ScheduleExt.kt | 6 +- .../provider/GTFSRealTimeProvider.java | 9 + .../provider/gtfs/GTFSRDSProviderExt.kt | 27 +- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 18 +- .../provider/gtfs/GtfsStatusProviderExt.kt | 34 +- .../status/GTFSRealTimeTripUpdatesProvider.kt | 425 ++---------------- .../GTFSRealTimeTripUpdatesProviderExt.kt | 37 +- .../android/commons/data/ScheduleExtTests.kt | 2 +- .../GTFSRealTimeTripUpdatesProviderTests.kt | 253 ++++++++--- 11 files changed, 328 insertions(+), 502 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/POIStatus.java b/src/main/java/org/mtransit/android/commons/data/POIStatus.java index c14e28dc..43ea7ce2 100644 --- a/src/main/java/org/mtransit/android/commons/data/POIStatus.java +++ b/src/main/java/org/mtransit/android/commons/data/POIStatus.java @@ -48,9 +48,9 @@ protected static int getDefaultStatusTextColor(@NonNull Context context) { private final int type; private final long lastUpdateInMs; private final long maxValidityInMs; - private final long readFromSourceAtInMs; + private long readFromSourceAtInMs; @Nullable - private final String sourceLabel; + private String sourceLabel; private final boolean noData; public POIStatus( @@ -181,6 +181,10 @@ public String getSourceLabel() { return this.sourceLabel; } + public void setSourceLabel(@Nullable String sourceLabel) { + this.sourceLabel = sourceLabel; + } + @Nullable private String getExtrasJSONString() { try { @@ -232,4 +236,8 @@ public long getMaxValidityInMs() { public long getReadFromSourceAtInMs() { return readFromSourceAtInMs; } + + public void setReadFromSourceAtInMs(long readFromSourceAtInMs) { + this.readFromSourceAtInMs = readFromSourceAtInMs; + } } 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 deb64d2b..82889676 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -868,8 +868,13 @@ public String getLogTag() { @Nullable private Integer maxDataRequests = null; + @Deprecated(forRemoval = true) public ScheduleStatusFilter(@NonNull String targetUUID, @NonNull RouteDirectionStop rds) { - super(POI.ITEM_STATUS_TYPE_SCHEDULE, targetUUID); + this(rds); + } + + public ScheduleStatusFilter(@NonNull RouteDirectionStop rds) { + super(POI.ITEM_STATUS_TYPE_SCHEDULE, rds.getUUID()); this.routeDirectionStop = rds; } diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 84b13d95..17f109a8 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -6,12 +6,12 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant -fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null): Schedule.Timestamp { - return Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { +fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null, stopSequence: Int? = null) = + Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { arrival?.let { this.arrival = it } tripId?.let { this.tripId = it } + stopSequence?.let { this.setStopSequence(it) } } -} var Schedule.Timestamp.departure: Instant get() = departureT.millisToInstant() diff --git a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java index a7d958ae..5cd81a6d 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -112,6 +112,7 @@ public String getLogTag() { private static UriMatcher getNewUriMatcher(String authority) { UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); ServiceUpdateProvider.append(URI_MATCHER, authority); + StatusProvider.append(URI_MATCHER, authority); VehicleLocationProvider.append(URI_MATCHER, authority); return URI_MATCHER; } @@ -1504,6 +1505,10 @@ public Cursor queryMT(@NonNull Uri uri, @Nullable String[] projection, @Nullable if (cursor != null) { return cursor; } + cursor = StatusProvider.queryS(this, uri, selection); + if (cursor != null) { + return cursor; + } cursor = VehicleLocationProvider.queryS(this, uri, selection); if (cursor != null) { return cursor; @@ -1518,6 +1523,10 @@ public String getTypeMT(@NonNull Uri uri) { if (type != null) { return type; } + type = StatusProvider.getTypeS(this, uri); + if (type != null) { + return type; + } type = VehicleLocationProvider.getTypeS(this, uri); if (type != null) { return type; diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt index 6f3d52b5..02c3185e 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSRDSProviderExt.kt @@ -21,18 +21,21 @@ fun Context.getRDS( POIProviderContract.POI_PATH ), GTFSProviderContract.PROJECTION_RDS_POI, - buildString { - append( - SqlUtils.getWhereEquals( - GTFSProviderContract.RouteDirectionStopColumns.T_ROUTE_K_ID, - routeId - ) - ) - directionId?.let { - append(SqlUtils.AND) - append(SqlUtils.getWhereEquals(GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_K_ID, it)) - } - }, + POIProviderContract.Filter.toJSON( + POIProviderContract.Filter.getNewSqlSelectionFilter( + buildString { + append( + SqlUtils.getWhereEquals( + GTFSProviderContract.RouteDirectionStopColumns.T_ROUTE_K_ID, + routeId + ) + ) + directionId?.let { + append(SqlUtils.AND) + append(SqlUtils.getWhereEquals(GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_K_ID, it)) + } + }) + ).toString(), null, SqlUtils.getSortOrderAscending(GTFSProviderContract.RouteDirectionStopColumns.T_DIRECTION_STOPS_K_STOP_SEQUENCE) ).use { cursor -> diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt index 09bca551..5a7040fc 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GtfsRealtimeExt.kt @@ -2,6 +2,9 @@ package org.mtransit.android.commons.provider.gtfs import com.google.transit.realtime.TripUpdateKt import com.google.transit.realtime.TripUpdateKt.StopTimeEventKt +import com.google.transit.realtime.alertOrNull +import com.google.transit.realtime.tripUpdateOrNull +import com.google.transit.realtime.vehicleOrNull import org.mtransit.android.commons.Constants import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.secsToInstant @@ -39,6 +42,17 @@ object GtfsRealtimeExt { } } + @JvmStatic + fun GFeedEntity.toStringExt(debug: Boolean = Constants.DEBUG) = buildString { + append("FeedEntity:") + append("{") + append("id:").append(id).append(", ") + tripUpdateOrNull?.let { append(it.toStringExt(debug)).append(", ") } + vehicleOrNull?.let { append(it.toStringExt(debug)).append(", ") } + alertOrNull?.let { append(it.toStringExt(debug)).append(", ") } + append("}") + } + @JvmStatic fun List.toTripUpdates(): List = this.filter { it.hasTripUpdate() }.map { it.tripUpdate }.distinct() @@ -190,8 +204,8 @@ object GtfsRealtimeExt { append("{") optStopSequence?.let { append("stopSeq=").append(stopSequence).append(", ") } optStopId?.let { append("stopId=").append(stopId).append(", ") } - optArrival?.let { append(it.toStringExt(short = true)).append(", ") } - optDeparture?.let { append(it.toStringExt(short = true)).append(", ") } + optArrival?.let { append("arrival=").append(it.toStringExt(short = true)).append(", ") } + optDeparture?.let { append("departure=").append(it.toStringExt(short = true)).append(", ") } optDepartureOccupancyStatus?.let { append("depOcc=").append(departureOccupancyStatus).append(", ") } optScheduleRelationship?.let { append("schedRel=").append(scheduleRelationship).append(", ") } optStopTimeProperties?.let { append(it.toStringExt(short = true)).append(", ") } 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 4c422047..ace35e7e 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 @@ -3,44 +3,38 @@ package org.mtransit.android.commons.provider.gtfs import android.content.Context import android.net.Uri import org.mtransit.android.commons.MTLog -import org.mtransit.android.commons.SqlUtils import org.mtransit.android.commons.UriUtils +import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.provider.status.StatusProviderContract +import kotlin.time.Duration.Companion.hours fun Context.getRDSSchedule( authority: String, - targetUUID: String, -): Schedule? = getRDSSchedule(authority, listOf(targetUUID))?.singleOrNull() + rdsList: Iterable, +) = rdsList.mapNotNull { + getRDSSchedule(authority, it) +} fun Context.getRDSSchedule( authority: String, - targetUUIDs: List, -): List? = try { + rds: RouteDirectionStop, +): Schedule? = try { contentResolver.query( Uri.withAppendedPath( UriUtils.newContentUri(authority), StatusProviderContract.STATUS_PATH ), StatusProviderContract.PROJECTION_STATUS, - buildString { - append( - append(SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_UUID, targetUUIDs)) - ) - }, + Schedule.ScheduleStatusFilter(rds).apply { + setLookBehindInMs(1.hours.inWholeMilliseconds) + setMaxDataRequests(3) // yesterday service ending + today + tomorrow? + }.let { it.toJSONStringStatic(it) }, null, null ).use { cursor -> - buildList { - if (cursor != null && cursor.count > 0) { - if (cursor.moveToFirst()) { - do { - Schedule.fromCursorWithExtra(cursor)?.let { - add(it) - } - } while (cursor.moveToNext()) - } - } + cursor?.takeIf { it.count > 0 && it.moveToFirst() }?.let { + Schedule.fromCursorWithExtra(it) } } } catch (e: Exception) { 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 41be6da8..5a93b525 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 @@ -9,35 +9,18 @@ import org.mtransit.android.commons.TimeUtils import org.mtransit.android.commons.data.POIStatus import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule -import org.mtransit.android.commons.data.arrival -import org.mtransit.android.commons.data.departure -import org.mtransit.android.commons.millisToInstant import org.mtransit.android.commons.provider.GTFSRealTimeProvider -import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteDirectionTagTargetUUID -import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyRouteTagTargetUUID -import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAgencyTagTargetUUID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isIGNORE_DIRECTION import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optArrival -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDeparture import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDirectionId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopId -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopSequence -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toTripUpdates -import org.mtransit.android.commons.provider.gtfs.agencyTag import org.mtransit.android.commons.provider.gtfs.getRDS import org.mtransit.android.commons.provider.gtfs.getRDSSchedule -import org.mtransit.android.commons.provider.gtfs.getTargetUUIDs import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.makeRequest import org.mtransit.android.commons.provider.gtfs.parseRouteId -import org.mtransit.android.commons.provider.gtfs.parseStopId import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.status.StatusProvider.cacheAllStatusesBulkLockDB import java.io.File @@ -47,16 +30,10 @@ import java.net.SocketException import java.net.UnknownHostException import javax.net.ssl.SSLHandshakeException import kotlin.math.min -import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -import kotlin.time.Instant import com.google.transit.realtime.GtfsRealtime.FeedMessage as GFeedMessage -import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship -import com.google.transit.realtime.GtfsRealtime.TripUpdate as GTripUpdate -import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent -import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate object GTFSRealTimeTripUpdatesProvider { @@ -95,36 +72,31 @@ object GTFSRealTimeTripUpdatesProvider { @JvmStatic fun GTFSRealTimeProvider.getCached(statusFilter: StatusProviderContract.Filter): POIStatus? { val filter = statusFilter as? Schedule.ScheduleStatusFilter ?: run { - MTLog.w(this, "getNewStatus() > Can't find new schedule without schedule filter!") + MTLog.w(this@getCached, "getCached() > Can't find new schedule without schedule filter!") return null } - // return (statusFilter as? Schedule.ScheduleStatusFilter)?.let { filter -> - // ( - return filter.routeDirectionStop.getTargetUUIDs(this, includeStopTags = true) - // ?: filter.routeDirection?.getTargetUUIDs(this, includeAgencyTag = INCLUDE_AGENCY_TAG) - // ?: filter.route?.getTargetUUIDs(this, includeAgencyTag = INCLUDE_AGENCY_TAG)) - .let { targetUUIDs -> - val tripIds = filter.targetAuthority.let { targetAuthority -> - filter.routeId.let { routeId -> - context?.getTripIds(targetAuthority, routeId, filter.directionId) - } - } - tripIds - ?.takeIf { tripIds -> tripIds.isNotEmpty() } // trip IDs REQUIRED for GTFS Vehicle locations - ?.let { tripIds -> targetUUIDs to tripIds } - }?.let { (targetUUIDs, tripIds) -> - getCached(targetUUIDs, tripIds) - ?: makeCachedStatusFromAgencyData(filter, tripIds) + val tripIds = filter.targetAuthority.let { targetAuthority -> + filter.routeId.let { routeId -> + context?.getTripIds(targetAuthority, routeId, filter.directionId) } + }?.takeIf { tripIds -> tripIds.isNotEmpty() } // trip IDs REQUIRED for GTFS Trip Updates + ?: return null + return getCachedStatusS(filter.targetUUID, tripIds) + ?: makeCachedStatusFromAgencyData(filter, tripIds) } val GTFSRealTimeProvider.ignoreDirection get() = isIGNORE_DIRECTION(this.requireContextCompat()) - private fun GTFSRealTimeProvider.makeCachedStatusFromAgencyData(filter: Schedule.ScheduleStatusFilter, tripIds: List): POIStatus? { + private fun GTFSRealTimeProvider.makeCachedStatusFromAgencyData( + filter: Schedule.ScheduleStatusFilter, + tripIds: List + ): POIStatus? { val context = context ?: return null + val readFromSourceMs = GtfsRealTimeStorage.getTripUpdateLastUpdateMs(context, 0L) + if (readFromSourceMs <= 0L) return null // never loaded try { val rds = filter.routeDirectionStop - val targetAuthority = filter.targetAuthority + val targetAuthority = rds.authority val routeId = rds.route.id val directionId = rds.direction.id var sortedRDS: List? = null @@ -146,289 +118,45 @@ object GTFSRealTimeTripUpdatesProvider { return@filter true }.takeIf { it.isNotEmpty() } rdTripUpdates ?: return null + if (Constants.DEBUG) { + rdTripUpdates.forEach { (_, gTripUpdate) -> + MTLog.d( + this@GTFSRealTimeTripUpdatesProvider, + "makeCachedStatusFromAgencyData() > GTFS '${rds.direction.id}' trip update: ${gTripUpdate.toStringExt()}." + ) + } + } if (sortedRDS == null) { - sortedRDS = context.getRDS(this.authority, routeId, directionId) + sortedRDS = context.getRDS(rds.authority, routeId, directionId) } if (uuidSchedule == null) { uuidSchedule = sortedRDS ?.let { rdsList -> context - .getRDSSchedule(targetAuthority, rdsList.map { it.uuid }) - ?.associate { - it.targetUUID to it - } + .getRDSSchedule(targetAuthority, rdsList) + .associateBy { it.targetUUID } } } - if (true) { - uuidSchedule ?: return null - sortedRDS ?: return null - wip(rdTripUpdates, uuidSchedule, sortedRDS) - return null + uuidSchedule ?: return null + sortedRDS ?: return null + wip(rdTripUpdates, uuidSchedule, sortedRDS) + uuidSchedule.values.filterNotNull().forEach { schedule -> + if (!schedule.timestamps.any { it.isRealTime }) return@forEach + schedule.sourceLabel = "GTFS-RT" // FIXME + schedule.readFromSourceAtInMs = readFromSourceMs + cacheStatus(schedule) } - rdTripUpdates.forEach { (trip, gTripUpdate) -> - val updatedTripID = parseTripId(trip) ?: return@forEach - val stopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } - ?: return@forEach - val targetUuidOnThisTrip = uuidSchedule - ?.filter { (_, schedule) -> schedule?.timestamps?.any { it.tripId == updatedTripID } == true } - ?: return@forEach - val sortedRDSOnThisTrip = sortedRDS - ?.filter { rds -> targetUuidOnThisTrip.contains(rds.uuid) } - ?: return@forEach - var currentStopIdHash: String? = null - var currentStopSequence: Int? = null - var currentStopTimeIndex: Int = 0 - var currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) - var nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) - if (true) { - var rdsI = 0 - var stuI = 0 - var currentRDS: RouteDirectionStop? = sortedRDSOnThisTrip.getOrNull(rdsI) ?: return@forEach - var currentStopTimeUpdate: GTUStopTimeUpdate = stopTimeUpdates.getOrNull(stuI) ?: return@forEach - var nextStopTimeUpdate = stopTimeUpdates.getOrNull(stuI + 1) - while (currentRDS != null - && !isSameStop(currentRDS, currentStopTimeUpdate) - && rdsI <= sortedRDSOnThisTrip.size // we do want NULL - ) { - currentRDS = sortedRDSOnThisTrip.getOrNull(++rdsI) - } - currentRDS ?: return@forEach // no match - // 1st trip stop matching 1st stop time update found - var currentRDSTripTimestamp = - targetUuidOnThisTrip[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == updatedTripID } - ?: return@forEach - var (currentArrivalDelay, currentDepartureDelay) = getDelay(currentStopTimeUpdate, currentRDSTripTimestamp) - applyDelay(currentRDSTripTimestamp, currentArrivalDelay, currentDepartureDelay) - currentArrivalDelay = null // only once for the matching stop - currentRDS = sortedRDSOnThisTrip.getOrNull(++rdsI) // NEXT ONE - ?: return@forEach // no more stop - while (currentRDS != null && nextStopTimeUpdate != null - && !isSameStop(currentRDS, nextStopTimeUpdate) - ) { - // keep using current - currentRDSTripTimestamp = - targetUuidOnThisTrip[currentRDS.uuid]?.timestamps?.singleOrNull { it.tripId == updatedTripID } - ?: continue // FIXME??? - applyDelay(currentRDSTripTimestamp, currentArrivalDelay, currentDepartureDelay) - currentRDS = sortedRDSOnThisTrip.getOrNull(++rdsI) // NEXT ONE - } - currentRDS ?: return@forEach // no more RDS - - currentStopTimeUpdate = stopTimeUpdates.getOrNull(++stuI) ?: return@forEach - nextStopTimeUpdate = stopTimeUpdates.getOrNull(stuI + 1) - getDelay(currentStopTimeUpdate, currentRDSTripTimestamp).let { - currentArrivalDelay = it.first - currentDepartureDelay = it.second - } - return@forEach - } - var generatedStopSequence = 1 - sortedRDSOnThisTrip.forEach { rds -> - generatedStopSequence++ - currentStopIdHash = rds.stop.originalIdHashString - currentStopSequence = generatedStopSequence - if (false) { - findCurrentNextStopTimeUpdate(sortedRDSOnThisTrip, stopTimeUpdates, currentStopIdHash, currentStopSequence, currentStopTimeIndex).let { - currentStopTimeUpdate = it.first - nextStopTimeUpdate = it.second - } - currentStopTimeUpdate?.optStopSequence?.let { currentStopTimeUpdateStopSequence -> - when { - currentStopSequence < currentStopTimeUpdateStopSequence -> return@forEach // no real-time info yet - currentStopSequence > currentStopTimeUpdateStopSequence -> { - nextStopTimeUpdate?.optStopSequence?.let { nextStopTimeUpdateStopSequence -> - if (currentStopSequence < nextStopTimeUpdateStopSequence) { - // keep current stop time update - } else { - currentStopTimeIndex++ - currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) - nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) - } - } // ELSE keep current stop time update - } - - else -> currentStopTimeIndex = 0 - } - } - } - - - } - } - return null + return getCachedStatusS(filter.targetUUID, tripIds) } catch (e: Exception) { MTLog.w(this, e, "makeCachedStatusFromAgencyData() > error!") return null } } - - fun getDelay( - stopTimeUpdate: GTUStopTimeUpdate?, - timestamp: Schedule.Timestamp, - previousDelays: Pair = null to null, - ): Pair { - stopTimeUpdate ?: return null to null // no delay info // show static schedule info - when (stopTimeUpdate.optScheduleRelationship) { - null, // DEFAULT - GTUStopTimeUpdate.ScheduleRelationship.SCHEDULED -> { - } // DO NOTHING - GTUStopTimeUpdate.ScheduleRelationship.NO_DATA -> { - // keep static, forget current stop time update - return null to null // no delay info // show static schedule info - } - - GTUStopTimeUpdate.ScheduleRelationship.SKIPPED -> { - // TODO remove trip timestamp (stop will not be stopped ad) - MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: SKIPPED") - // return null // stop will be skipped - return previousDelays - } - - GTUStopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule - MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: UNSCHEDULED") - } - } - val timestampOriginalArrival = timestamp.arrival - val timestampOriginalDeparture = timestamp.departure - var departureDelay: Duration? = stopTimeUpdate.optDeparture?.makeDelay(timestampOriginalDeparture) - val arrivalDelay: Duration? = stopTimeUpdate.optArrival?.makeDelay(timestampOriginalArrival) - if (departureDelay == null && arrivalDelay != null) { - departureDelay = timestampOriginalDeparture.coerceAtLeast(timestampOriginalArrival + arrivalDelay) - timestampOriginalDeparture - } - return arrivalDelay to departureDelay - } - - fun applyDelay( - timestamp: Schedule.Timestamp, - arrivalDelay: Duration?, - departureDelay: Duration?, - ) = timestamp.apply { - departureDelay?.let { departure += it } // 1st - arrivalDelay?.let { arrival += it } // 2nd - } - - fun applyUpdate( - timestamp: Schedule.Timestamp, - currentStopTimeUpdate: GTUStopTimeUpdate? - ): Schedule.Timestamp? { - currentStopTimeUpdate ?: return timestamp // no change - when (currentStopTimeUpdate.optScheduleRelationship) { - null, // DEFAULT - GTUStopTimeUpdate.ScheduleRelationship.SCHEDULED -> { - } // DO NOTHING - GTUStopTimeUpdate.ScheduleRelationship.NO_DATA -> { - // keep static, forget current stop time update - return timestamp // no change - } - - GTUStopTimeUpdate.ScheduleRelationship.SKIPPED -> { - // TODO remove trip timestamp (stop will not be stopped ad) - MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: SKIPPED") - return null // stop will be skipped - } - - GTUStopTimeUpdate.ScheduleRelationship.UNSCHEDULED -> { // only with frequency based schedule - MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "Unexpected stop time schedule relationship: UNSCHEDULED") - } - } - val timestampOriginalArrival = timestamp.arrivalT.millisToInstant() - val timestampOriginalDeparture = timestamp.departureT.millisToInstant() - val departureDelay: Duration? = currentStopTimeUpdate.optDeparture?.makeDelay(timestamp.departureT.millisToInstant()) - val arrivalDelay: Duration? = currentStopTimeUpdate.optArrival?.makeDelay(timestamp.arrivalT.millisToInstant()) - - TODO() - } - - private fun GTUStopTimeEvent.makeDelay(originalTime: Instant): Duration? = - optDelay?.seconds - ?: optTimeInstant?.let { time -> time - originalTime } - - fun GTFSRealTimeProvider.findCurrentNextStopTimeUpdate( - sortedRDS: List, - stopTimeUpdates: List?, - currentStopIdHash: String?, - currentStopSequence: Int, - currentStopTimeIndex: Int, - ): Pair { - var currentStopTimeIndex = currentStopTimeIndex - var currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) - var nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) - currentStopTimeUpdate?.let { - - } - if (false) { // TODO later stop sequence - currentStopTimeUpdate?.optStopSequence?.let { currentStopTimeUpdateStopSequence -> - while (true) { - if (currentStopSequence < currentStopTimeUpdateStopSequence) { - return null to null // no real-time info yet - } - if (currentStopSequence == currentStopTimeUpdateStopSequence) { - return currentStopTimeUpdate to nextStopTimeUpdate // use - } - if (currentStopSequence > currentStopTimeUpdateStopSequence) { - nextStopTimeUpdate?.optStopSequence?.let { nextStopTimeUpdateStopSequence -> - if (currentStopSequence < nextStopTimeUpdateStopSequence) { - return currentStopTimeUpdate to nextStopTimeUpdate // keep same - } else if (currentStopSequence == nextStopTimeUpdateStopSequence) { - currentStopTimeIndex++ - currentStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex) - nextStopTimeUpdate = stopTimeUpdates?.getOrNull(currentStopTimeIndex + 1) - // continue - return currentStopTimeUpdate to nextStopTimeUpdate // use next - } else { - currentStopTimeIndex++ - } - } - } - } - } - } - TODO("Not yet implemented") - } - - fun GTFSRealTimeProvider.getStopTimeUpdateSequence( - sortedRDS: List, - stopTimeUpdate: GTUStopTimeUpdate - ): Int? { - val providedStopSequence = stopTimeUpdate.optStopSequence - val providedStopIdHash = parseStopId(stopTimeUpdate) - // .optStopId?.originalIdToHash(stopIdCleanupPattern) - ?: return providedStopSequence - if (providedStopSequence == null) { - return sortedRDS.indexOfFirst { rds -> isSameStop(rds, stopTimeUpdate) } - } - // providedStopSequence?.let { - // return it - // } - // TODO HERE NOW, it's complicated, trip stop sequence can be a mess - // TODO: only guarantee is stop order... if stop not repeated in same trip - // TODO -> maybe start simple first by using stop ID if available then stop sequence and ignore complex use case data for now - var iRDS = 0 - var generatedStopSequence = 1 - while (iRDS < sortedRDS.size) { - // for (; iRDS < sortedRDS.size; iRDS++) { - val currentRDS = sortedRDS[iRDS] - if (isSameStop(currentRDS, stopTimeUpdate)) { - if (generatedStopSequence == providedStopSequence) { - return generatedStopSequence - } - generatedStopSequence++ - } else { - break - } - } - return null - } - - fun GTFSRealTimeProvider.getCached(targetUUIDs: Map, tripIds: List): POIStatus? { - return getCachedStatusS(targetUUIDs.keys, tripIds) - } - @JvmStatic fun GTFSRealTimeProvider.getNew(statusFilter: StatusProviderContract.Filter): POIStatus? { val filter = statusFilter as? Schedule.ScheduleStatusFilter ?: run { - MTLog.w(this, "getNewStatus() > Can't find new schedule without schedule filter!") + MTLog.w(this, "getNew() > Can't find new schedule without schedule filter!") return null } updateAgencyDataIfRequired(filter.isInFocusOrDefault) @@ -547,85 +275,4 @@ object GTFSRealTimeTripUpdatesProvider { return null } } - - private fun GTFSRealTimeProvider.processTripUpdates( - newLastUpdateInMs: Long, - gTripUpdate: GTripUpdate, - ignoreDirection: Boolean, - ): Set? { - val updateRouteId = gTripUpdate.optTrip?.let { parseRouteId(it) } - val updateDirectionId = gTripUpdate.optTrip?.optDirectionId - ?.takeIf { !ignoreDirection } - val updatedTripId = gTripUpdate.optTrip?.let { parseTripId(it) } - gTripUpdate.optDelay?.let { - // experimental field, means all stop times are delayed - // -> fetch all trips stops static schedule and generate real-time schedule with delay - } - gTripUpdate.optStopTimeUpdateList?.forEach { stopTimeUpdate -> - stopTimeUpdate.optStopId?.let { stopId -> - // val targetUUIDs = RouteDirectionStop.makeUUID() - } ?: run { - // NO STOP ID provided > not supported, original trip ID "stop sequence" is not in the local DB! - } - } - val targetUUIDs = parseProviderTargetUUID(gTripUpdate, ignoreDirection)?.takeIf { it.isNotBlank() } ?: return null - return setOf( - Schedule( - null, - targetUUIDs, - newLastUpdateInMs, - maxValidityInMs, - newLastUpdateInMs, - PROVIDER_PRECISION_IN_MS, - false, // noPickup - null, // sourceLabel - false // no data - // - // authority = this.authority, - // targetUUID = targetUUIDs, - // targetTripId = gTripUpdate.optTrip?.optTripId?.originalIdToId(tripIdCleanupPattern), - // lastUpdateInMs = newLastUpdateInMs, - // maxValidityInMs = this@processTripUpdates.vehicleLocationMaxValidityInMs, - // // - // vehicleId = gTripUpdate.optVehicle?.optId, - // vehicleLabel = gTripUpdate.optVehicle?.optLabel, - // reportTimestamp = gTripUpdate.optTimestamp?.secsToInstant(), - // latitude = gTripUpdate.optPosition?.optLatitude ?: return null, - // longitude = gTripUpdate.optPosition?.optLongitude ?: return null, - // bearingDegrees = gTripUpdate.optPosition?.optBearing?.toInt(), // in degrees - // speedMetersPerSecond = gTripUpdate.optPosition?.optSpeed?.toInt(), // in meters per second - ) - ) - } - - private fun GTFSRealTimeProvider.parseProviderTargetUUID(gTripUpdate: GTripUpdate, ignoreDirection: Boolean): String? { - val gTripDescriptor = gTripUpdate.optTrip ?: return null - if (gTripDescriptor.hasModifiedTrip()) { - MTLog.d(this, "parseTargetUUID() > unhandled modified trip: ${gTripDescriptor.toStringExt()}") - } - if (gTripDescriptor.hasStartTime() || gTripDescriptor.hasStartDate()) { - MTLog.d(this, "parseTargetUUID() > unhandled start date & time: ${gTripDescriptor.toStringExt()}") - } - when (gTripDescriptor.scheduleRelationship) { - GTDScheduleRelationship.SCHEDULED -> {} // handled - GTDScheduleRelationship.ADDED, - GTDScheduleRelationship.UNSCHEDULED, - GTDScheduleRelationship.CANCELED, - GTDScheduleRelationship.REPLACEMENT, - GTDScheduleRelationship.DUPLICATED, - GTDScheduleRelationship.DELETED, - GTDScheduleRelationship.NEW, - -> MTLog.d(this, "parseTargetUUID() > unhandled schedule relationship: ${gTripDescriptor.scheduleRelationship}") - } - parseRouteId(gTripDescriptor)?.let { routeId -> - gTripDescriptor.optDirectionId?.takeIf { !ignoreDirection }?.let { directionId -> - return getAgencyRouteDirectionTagTargetUUID(agencyTag, routeId, directionId) - } - return getAgencyRouteTagTargetUUID(agencyTag, routeId) - } - return getAgencyTagTargetUUID(agencyTag) - } - - private fun GTFSRealTimeProvider.isSameStop(rds: RouteDirectionStop, stopTimeUpdate: GTUStopTimeUpdate) = - rds.stop.isSameOriginalId(parseStopId(stopTimeUpdate)) } 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 7304770c..d2edb508 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 @@ -1,5 +1,6 @@ package org.mtransit.android.commons.provider.status +import org.mtransit.android.commons.TimeUtilsK import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.data.arrival @@ -101,7 +102,7 @@ internal fun wipTripUpdate( var uuidAndSeqIdx = 0 var currentDelay = gTripUpdate.optDelay?.seconds // initial delay valid until 1st stop time update val gStopTimeUpdates = gTripUpdate.optStopTimeUpdateList?.sortedBy { it.optStopSequence } - var currentStopTimeUpdate: GTUStopTimeUpdate? = null + var currentStopTimeUpdate: GTUStopTimeUpdate? var nextStopTimeUpdate: GTUStopTimeUpdate? = gStopTimeUpdates?.getOrNull(stuIdx) var currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(uuidAndSeqIdx) ?: return // no more stop @@ -111,14 +112,14 @@ internal fun wipTripUpdate( while (!isSameStop(nextStopTimeUpdate, currentRDS, currentUuidAndSeq.second) && uuidAndSeqIdx <= sortedTargetUuidAndSequence.size // allow null currentRDS to signify end of trip ) { - currentDelay = wipApplyDelay(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentDelay) + currentDelay = wipApplyDelay(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], 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 = wipApplyDelaySTU(tripId, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) + currentDelay = wipApplyDelaySTU(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } @@ -126,12 +127,23 @@ internal fun wipTripUpdate( internal fun wipApplyDelaySTU( tripId: String, + stopSequence: Int, rdsSchedule: Schedule?, gStopTimeUpdate: GTUStopTimeUpdate, currentDelay: Duration? = null, ): Duration? { - val rdsTripTimestamp = rdsSchedule?.timestamps - ?.singleOrNull { it.tripId == tripId } // TODO handle multiple stops at this stop during same trip + val rdsTripTimestamp = rdsSchedule + ?.timestamps?.filter { it.tripId == tripId } + ?.filter { timestamp -> + (timestamp.stopSequenceOrNull == null || timestamp.stopSequenceOrNull == stopSequence) + }?.let { rdsTripTimestamps -> + if (rdsTripTimestamps.size > 1) { + val now = TimeUtilsK.currentInstant() + rdsTripTimestamps.sortedBy { (it.departure - now).absoluteValue } + } else { + rdsTripTimestamps + }.firstOrNull() + } ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure @@ -169,12 +181,23 @@ internal fun GTUStopTimeEvent?.wipMakeDelay( internal fun wipApplyDelay( tripId: String, + stopSequence: Int, rdsSchedule: Schedule?, currentDelay: Duration? ): Duration? { currentDelay ?: return null - val rdsTripTimestamp = rdsSchedule?.timestamps - ?.singleOrNull { it.tripId == tripId } // TODO handle multiple stops at this stop during same trip + val rdsTripTimestamp = rdsSchedule + ?.timestamps?.filter { it.tripId == tripId } + ?.filter { timestamp -> + (timestamp.stopSequenceOrNull == null || timestamp.stopSequenceOrNull == stopSequence) + }?.let { rdsTripTimestamps -> + if (rdsTripTimestamps.size > 1) { + val now = TimeUtilsK.currentInstant() + rdsTripTimestamps.sortedBy { (it.departure - now).absoluteValue } + } else { + rdsTripTimestamps + }.firstOrNull() + } ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.departure - rdsTripTimestamp.arrival if (currentDelay < Duration.ZERO) { diff --git a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt index 3438c66e..6a07f28e 100644 --- a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt +++ b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt @@ -13,7 +13,7 @@ class ScheduleExtTests { } @Test - fun test1() { + fun test_departure_update_no_effect_on_arrival() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) 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 9dc482f7..6db1b82a 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 @@ -42,6 +42,7 @@ class GTFSRealTimeTripUpdatesProviderTests { private const val NOW_IN_MS = 123456789_000L private const val TRIP_ID = "123456789" + private const val STOP_SEQUENCE = 1 } // region same stop @@ -64,10 +65,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_null() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val delay: Duration? = null - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNull(result) assertFalse { timestamp.isRealTime } @@ -77,10 +78,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_0_on_time() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val delay = Duration.ZERO - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -91,10 +92,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelay_simple_late() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -106,10 +107,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_differentArrival_late() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(9.minutes, result) // delay partially consumed @@ -122,10 +123,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_consumed_late() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 15.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(Duration.ZERO, result) // delay consumed @@ -138,10 +139,10 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelay_simple_early() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val delay = (-5).minutes - val result = wipApplyDelay(TRIP_ID, timestamp.toSchedule(), delay) + val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -157,14 +158,14 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun text_applyDelaySTU_simple() { val departure = DEPARTURE_MS.secsToInstant() - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, tripId = TRIP_ID) + val timestamp = mkTime(departure) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { time = (departure + 1.minutes).toSecs() } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(1.minutes, result) @@ -176,7 +177,7 @@ class GTFSRealTimeTripUpdatesProviderTests { fun text_applyDelaySTU_2() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { this.arrival = stopTimeEvent { time = (arrival - 1.minutes).toSecs() @@ -186,7 +187,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(2.minutes, result) @@ -200,14 +201,14 @@ class GTFSRealTimeTripUpdatesProviderTests { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes val delay = 1.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { time = (departure + 2.minutes).toSecs() } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate, delay) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -221,14 +222,14 @@ class GTFSRealTimeTripUpdatesProviderTests { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val delay = 15.minutes // should be ignored - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { this.arrival = stopTimeEvent { time = (arrival + 3.minutes).toSecs() } } - val result = wipApplyDelaySTU(TRIP_ID, timestamp.toSchedule(), stopTimeUpdate, delay) + val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -271,7 +272,7 @@ class GTFSRealTimeTripUpdatesProviderTests { fun test_makeDelay_3() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 5.minutes - val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) + val timestamp = mkTime(departure, arrival = arrival) val previousDelay = 10.minutes val stopTimeEvent: GTUStopTimeEvent? = null @@ -291,7 +292,7 @@ class GTFSRealTimeTripUpdatesProviderTests { private val isSameStop: ((GTUStopTimeUpdate?, RouteDirectionStop?, Int) -> Boolean) = { stu, rds, stopSeq -> - stu?.isSameStop(rds, stopSeq, parseStopId = { it } ) == true + stu?.isSameStop(rds, stopSeq, parseStopId = { it }) == true } @Test @@ -309,9 +310,9 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart, TRIP_ID)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes, TRIP_ID)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes, TRIP_ID)))) } + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 10.minutes)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(tripStart + 20.minutes)))) } } val sortedTargetUuidAndSequence = buildList { rdsList.forEachIndexed { index, rds -> @@ -343,11 +344,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_combined_complex_stop_id() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID } delayDuration = 1.minutes stopTimeUpdate += stopTimeUpdate { @@ -384,16 +384,16 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 10000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } - rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId)))) } - rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, arrival = startsAt + 37.minutes)))) } - rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId)))) } - rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId)))) } - rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId)))) } - rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId)))) } - rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId)))) } + 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)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes)))) } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes)))) } + rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes)))) } } val sortedTargetUuidAndSequence = buildList { rdsList.forEachIndexed { index, rds -> @@ -468,11 +468,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_combined_complex_stop_sequence() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID } delayDuration = 1.minutes stopTimeUpdate += stopTimeUpdate { @@ -510,16 +509,16 @@ class GTFSRealTimeTripUpdatesProviderTests { } var stopSeq = 0 val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId, ++stopSeq)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId, ++stopSeq)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId, ++stopSeq)))) } - rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, tripId, ++stopSeq)))) } - rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, tripId, ++stopSeq, arrival = startsAt + 37.minutes)))) } - rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, tripId, ++stopSeq)))) } - rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, tripId, ++stopSeq)))) } - rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, tripId, ++stopSeq)))) } - rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, tripId, ++stopSeq)))) } - rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, tripId, ++stopSeq)))) } + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, stopSeq = ++stopSeq)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, stopSeq = ++stopSeq)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, stopSeq = ++stopSeq)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, stopSeq = ++stopSeq)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, stopSeq = ++stopSeq, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, stopSeq = ++stopSeq)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, stopSeq = ++stopSeq)))) } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 70.minutes, stopSeq = ++stopSeq)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, stopSeq = ++stopSeq)))) } + rdsList[9].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, stopSeq = ++stopSeq)))) } } val sortedTargetUuidAndSequence = buildList { tripTargetUuidSchedule.forEach { (uuid, schedule) -> @@ -594,13 +593,142 @@ class GTFSRealTimeTripUpdatesProviderTests { } } + @Test + fun test_wipTripUpdate_combined_complex_stop_sequence_repeated_stop() { + val startsAt = DEPARTURE_MS.secsToInstant() + val gTripUpdate = tripUpdate { + trip = tripDescriptor { + tripId = TRIP_ID + } + delayDuration = 1.minutes + stopTimeUpdate += stopTimeUpdate { + stopSequence = 2 + departure = stopTimeEvent { + delayDuration = 3.minutes + } + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 4 + arrival = stopTimeEvent { + time = ((startsAt + 30.minutes) + 5.minutes).toSecs() + } + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 7 + scheduleRelationship = GTUSTUScheduleRelationship.SKIPPED + } + stopTimeUpdate += stopTimeUpdate { + stopSequence = 9 + scheduleRelationship = GTUSTUScheduleRelationship.NO_DATA + } + } + val rdsList = buildList { + add(makeRDS(stopId = 1000)) + add(makeRDS(stopId = 2000)) + add(makeRDS(stopId = 3000)) + add(makeRDS(stopId = 4000)) + add(makeRDS(stopId = 5000)) + add(makeRDS(stopId = 6000)) + add(makeRDS(stopId = 7000)) + add(makeRDS(stopId = 9000)) + add(makeRDS(stopId = 10000)) + } + var stopSeq = 0 + val tripTargetUuidSchedule = buildMap { + rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, stopSeq = ++stopSeq)))) } + rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, stopSeq = ++stopSeq)))) } + rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, stopSeq = ++stopSeq)))) } + rdsList[3].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 30.minutes, stopSeq = ++stopSeq)))) } + rdsList[4].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 43.minutes, stopSeq = ++stopSeq, arrival = startsAt + 37.minutes)))) } + rdsList[5].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 50.minutes, stopSeq = ++stopSeq)))) } + rdsList[6].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 60.minutes, stopSeq = ++stopSeq)))) } + get(rdsList[3].uuid)?.apply { + addTimestampWithoutSort(mkTime(startsAt + 70.minutes, stopSeq = ++stopSeq)) + sortTimestamps() + } + rdsList[7].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 80.minutes, stopSeq = ++stopSeq)))) } + rdsList[8].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 90.minutes, stopSeq = ++stopSeq)))) } + } + val sortedTargetUuidAndSequence = buildList { + tripTargetUuidSchedule.forEach { (uuid, schedule) -> + schedule?.timestamps?.forEach { timestamp -> + add(uuid to assertNotNull(timestamp.stopSequenceOrNull)) + } + } + }.sortedBy { (_, stopSequence) -> stopSequence } + + + wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + + assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 1.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 10.minutes + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 10.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[2].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 20.minutes + 3.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[3].uuid]) { schedule -> + assertNotNull(schedule.timestamps.getOrNull(0)) { timestamp -> + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 30.minutes + 5.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[4].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 37.minutes + 5.minutes, timestamp.arrival) + assertEquals(startsAt + 43.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[5].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 50.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[6].uuid]) { schedule -> + assertTrue { schedule.timestamps.isEmpty() } + } + assertNotNull(tripTargetUuidSchedule[rdsList[3].uuid]) { schedule -> + assertNotNull(schedule.timestamps.getOrNull(1)) { timestamp -> + assertEquals(startsAt + 70.minutes, timestamp.departure) + assertTrue { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[7].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 80.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + assertNotNull(tripTargetUuidSchedule[rdsList[8].uuid]) { schedule -> + assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> + assertEquals(startsAt + 90.minutes, timestamp.departure) + assertFalse { timestamp.isRealTime } + } + } + } + @Test fun test_wipTripUpdate_trip_cancelled() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID this.scheduleRelationship = GTDScheduleRelationship.CANCELED } delayDuration = 1.minutes @@ -611,9 +739,9 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + 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 -> @@ -636,11 +764,10 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_wipTripUpdate_trip_deleted() { - val tripId = "123456789" val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { - this.tripId = tripId + tripId = TRIP_ID this.scheduleRelationship = GTDScheduleRelationship.DELETED } delayDuration = 1.minutes @@ -651,9 +778,9 @@ class GTFSRealTimeTripUpdatesProviderTests { add(makeRDS(stopId = 3000)) } val tripTargetUuidSchedule = buildMap { - rdsList[0].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt, tripId)))) } - rdsList[1].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 10.minutes, tripId)))) } - rdsList[2].uuid.let { put(it, mkSchedule(it, listOf(mkTime(startsAt + 20.minutes, tripId)))) } + 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 -> @@ -681,12 +808,8 @@ class GTFSRealTimeTripUpdatesProviderTests { ) @Suppress("SameParameterValue") - private fun mkTime(time: Instant, tripId: String?, stopSequence: Int? = null, arrival: Instant? = null) = - time.toScheduleTimestamp(LOCAL_TZ_ID, arrival, TRIP_ID) - .apply { - this.tripId = tripId - stopSequence?.let { this.setStopSequence(it) } - } + private fun mkTime(time: Instant, tripId: String? = TRIP_ID, stopSeq: Int? = null, arrival: Instant? = null) = + time.toScheduleTimestamp(LOCAL_TZ_ID, arrival, tripId, stopSeq) private fun mkSchedule( targetUuid: String = makeRDS().uuid, From fc8fa3b0226f29a5940dc6b82ef70e5a182d3668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 10:01:53 -0400 Subject: [PATCH 22/39] cleanup --- .../status/GTFSRealTimeTripUpdatesProvider.kt | 2 +- .../GTFSRealTimeTripUpdatesProviderExt.kt | 20 ++++---- .../GTFSRealTimeTripUpdatesProviderTests.kt | 50 +++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) 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 5a93b525..e209bf8f 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 @@ -140,7 +140,7 @@ object GTFSRealTimeTripUpdatesProvider { } uuidSchedule ?: return null sortedRDS ?: return null - wip(rdTripUpdates, uuidSchedule, sortedRDS) + processRDTripUpdates(rdTripUpdates, uuidSchedule, sortedRDS) uuidSchedule.values.filterNotNull().forEach { schedule -> if (!schedule.timestamps.any { it.isRealTime }) return@forEach schedule.sourceLabel = "GTFS-RT" // FIXME 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 d2edb508..c0053a5a 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 @@ -29,7 +29,7 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUS import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUSTUScheduleRelationship -fun GTFSRealTimeProvider.wip( +fun GTFSRealTimeProvider.processRDTripUpdates( rdTripUpdates: List>, targetUuidSchedule: Map, sortedRDS: List @@ -46,7 +46,7 @@ fun GTFSRealTimeProvider.wip( .takeIf { it.isNotEmpty() } ?: return@forEach val sortedTargetUuidAndSequence = makeTargetUuidAndSequenceList(tripId, tripTargetUuidSchedule, tripSortedRDS) - wipTripUpdate( + processRDTripUpdate( tripId, gTripUpdate, tripSortedRDS, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop = { stu, rds, stopSeq -> isSameStop(stu, rds, stopSeq) }, ) @@ -80,7 +80,7 @@ internal fun makeTargetUuidAndSequenceList( }.sortedBy { (_, stopSequence) -> stopSequence } } -internal fun wipTripUpdate( +internal fun processRDTripUpdate( tripId: String, gTripUpdate: GTripUpdate, tripSortedRDS: List, @@ -112,20 +112,20 @@ internal fun wipTripUpdate( while (!isSameStop(nextStopTimeUpdate, currentRDS, currentUuidAndSeq.second) && uuidAndSeqIdx <= sortedTargetUuidAndSequence.size // allow null currentRDS to signify end of trip ) { - currentDelay = wipApplyDelay(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentDelay) + currentDelay = applyDelay(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], 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 = wipApplyDelaySTU(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) + currentDelay = applyDelaySTU(tripId, currentUuidAndSeq.second, tripTargetUuidSchedule[currentRDS.uuid], currentStopTimeUpdate, currentDelay) currentUuidAndSeq = sortedTargetUuidAndSequence.getOrNull(++uuidAndSeqIdx) ?: break // no more stop currentRDS = tripSortedRDS.singleOrNull { it.uuid == currentUuidAndSeq.first } ?: break // stop not found! } } -internal fun wipApplyDelaySTU( +internal fun applyDelaySTU( tripId: String, stopSequence: Int, rdsSchedule: Schedule?, @@ -150,12 +150,12 @@ internal fun wipApplyDelaySTU( val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO val stuArrivalDelay = gStopTimeUpdate.optArrival .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } - .wipMakeDelay(timestampOriginalArrival) + .makeDelay(timestampOriginalArrival) ?: currentDelay .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } val stuDepartureDelay = gStopTimeUpdate.optDeparture .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } - .wipMakeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) + .makeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } if (gStopTimeUpdate.scheduleRelationship == GTUSTUScheduleRelationship.SKIPPED) { @@ -165,7 +165,7 @@ internal fun wipApplyDelaySTU( .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } } -internal fun GTUStopTimeEvent?.wipMakeDelay( +internal fun GTUStopTimeEvent?.makeDelay( originalTime: Instant, previousDelay: Duration? = null, previousOriginalDiff: Duration? = null, @@ -179,7 +179,7 @@ internal fun GTUStopTimeEvent?.wipMakeDelay( } } -internal fun wipApplyDelay( +internal fun applyDelay( tripId: String, stopSequence: Int, rdsSchedule: Schedule?, 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 6db1b82a..2ff22ed1 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 @@ -68,7 +68,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val timestamp = mkTime(departure) val delay: Duration? = null - val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) + val result = applyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNull(result) assertFalse { timestamp.isRealTime } @@ -81,7 +81,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val timestamp = mkTime(departure) val delay = Duration.ZERO - val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) + val result = applyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -95,7 +95,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val timestamp = mkTime(departure) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) + val result = applyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -110,7 +110,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) + val result = applyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(9.minutes, result) // delay partially consumed @@ -126,7 +126,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes - val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) + val result = applyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(Duration.ZERO, result) // delay consumed @@ -142,7 +142,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val timestamp = mkTime(departure, arrival = arrival) val delay = (-5).minutes - val result = wipApplyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) + val result = applyDelay(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), delay) assertNotNull(result) assertEquals(delay, result) // delay not consumed @@ -165,7 +165,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) + val result = applyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(1.minutes, result) @@ -187,7 +187,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) + val result = applyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) assertNotNull(result) assertEquals(2.minutes, result) @@ -208,7 +208,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) + val result = applyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -229,7 +229,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - val result = wipApplyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) + val result = applyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate, delay) assertNotNull(result) assertEquals(2.minutes, result) @@ -249,7 +249,7 @@ class GTFSRealTimeTripUpdatesProviderTests { delay = 10 } - val result = stopTimeEvent.wipMakeDelay(originalTime) + val result = stopTimeEvent.makeDelay(originalTime) assertNotNull(result) assertEquals(10.seconds, result) @@ -262,7 +262,7 @@ class GTFSRealTimeTripUpdatesProviderTests { time = (originalTime + 10.seconds).toSecs() } - val result = stopTimeEvent.wipMakeDelay(originalTime) + val result = stopTimeEvent.makeDelay(originalTime) assertNotNull(result) assertEquals(10.seconds, result) @@ -276,7 +276,7 @@ class GTFSRealTimeTripUpdatesProviderTests { val previousDelay = 10.minutes val stopTimeEvent: GTUStopTimeEvent? = null - val result = stopTimeEvent.wipMakeDelay( + val result = stopTimeEvent.makeDelay( originalTime = timestamp.departure, previousDelay = previousDelay, previousOriginalDiff = timestamp.arrivalDiff @@ -296,7 +296,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_singleTUDelay() { + fun test_processRDTripUpdate_singleTUDelay() { val tripStart = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -320,7 +320,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -343,7 +343,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_combined_complex_stop_id() { + fun test_processRDTripUpdate_combined_complex_stop_id() { val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -401,7 +401,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -467,7 +467,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_combined_complex_stop_sequence() { + fun test_processRDTripUpdate_combined_complex_stop_sequence() { val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -528,7 +528,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -594,7 +594,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_combined_complex_stop_sequence_repeated_stop() { + fun test_processRDTripUpdate_combined_complex_stop_sequence_repeated_stop() { val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -658,7 +658,7 @@ class GTFSRealTimeTripUpdatesProviderTests { }.sortedBy { (_, stopSequence) -> stopSequence } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> @@ -724,7 +724,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_trip_cancelled() { + fun test_processRDTripUpdate_trip_cancelled() { val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -749,7 +749,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertTrue { schedule.timestamps.isEmpty() } @@ -763,7 +763,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun test_wipTripUpdate_trip_deleted() { + fun test_processRDTripUpdate_trip_deleted() { val startsAt = DEPARTURE_MS.secsToInstant() val gTripUpdate = tripUpdate { trip = tripDescriptor { @@ -788,7 +788,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } } - wipTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) + processRDTripUpdate(TRIP_ID, gTripUpdate, rdsList, sortedTargetUuidAndSequence, tripTargetUuidSchedule, isSameStop) assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertTrue { schedule.timestamps.isEmpty() } From 7045645019b95e057b7f4c8b2df61199eaeaf61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 10:03:07 -0400 Subject: [PATCH 23/39] fix --- src/main/java/org/mtransit/android/commons/data/Schedule.java | 1 - 1 file changed, 1 deletion(-) 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 82889676..6fb95985 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -868,7 +868,6 @@ public String getLogTag() { @Nullable private Integer maxDataRequests = null; - @Deprecated(forRemoval = true) public ScheduleStatusFilter(@NonNull String targetUUID, @NonNull RouteDirectionStop rds) { this(rds); } From ebccecc81fd833ebe28ec0edb624c13982f0d0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 10:14:46 -0400 Subject: [PATCH 24/39] fix URL --- .../commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e209bf8f..89f71f7e 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 @@ -217,7 +217,7 @@ object GTFSRealTimeTripUpdatesProvider { try { val urlRequest = makeRequest( context, - urlCachedString = GTFSRealTimeProvider.getAGENCY_VEHICLE_POSITIONS_URL_CACHED(context), + urlCachedString = GTFSRealTimeProvider.getAGENCY_TRIP_UPDATES_URL_CACHED(context), getUrlString = { token -> GTFSRealTimeProvider.getAgencyTripUpdatesUrlString(context, token) } ) ?: return null getOkHttpClient(context).newCall(urlRequest).execute().use { response -> From 10e17dbfdcc5e569d087a6f2c576c088b7578469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 10:30:27 -0400 Subject: [PATCH 25/39] wip --- .../android/commons/data/Schedule.java | 6 ++++- .../status/GTFSRealTimeTripUpdatesProvider.kt | 25 +++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) 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 6fb95985..2697d73d 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -45,7 +45,7 @@ public String getLogTag() { @NonNull private final List timestamps = new ArrayList<>(); - private final long providerPrecisionInMs; + private long providerPrecisionInMs; private long usefulUntilInMs = -1L; @@ -93,6 +93,10 @@ public long getProviderPrecisionInMs() { return providerPrecisionInMs; } + public void setProviderPrecisionInMs(long providerPrecisionInMs) { + this.providerPrecisionInMs = providerPrecisionInMs; + } + @NonNull @Override public String toString() { 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 89f71f7e..a75056c6 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 @@ -23,6 +23,7 @@ import org.mtransit.android.commons.provider.gtfs.makeRequest import org.mtransit.android.commons.provider.gtfs.parseRouteId import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.status.StatusProvider.cacheAllStatusesBulkLockDB +import org.mtransit.commons.SourceUtils import java.io.File import java.io.IOException import java.net.HttpURLConnection @@ -94,6 +95,9 @@ object GTFSRealTimeTripUpdatesProvider { val context = context ?: return null val readFromSourceMs = GtfsRealTimeStorage.getTripUpdateLastUpdateMs(context, 0L) if (readFromSourceMs <= 0L) return null // never loaded + val sourceLabel = SourceUtils.getSourceLabel( // always use source from official API + GTFSRealTimeProvider.getAgencyTripUpdatesUrlString(context, "T") + ) try { val rds = filter.routeDirectionStop val targetAuthority = rds.authority @@ -122,7 +126,7 @@ object GTFSRealTimeTripUpdatesProvider { rdTripUpdates.forEach { (_, gTripUpdate) -> MTLog.d( this@GTFSRealTimeTripUpdatesProvider, - "makeCachedStatusFromAgencyData() > GTFS '${rds.direction.id}' trip update: ${gTripUpdate.toStringExt()}." + "makeCachedStatusFromAgencyData() > GTFS [R:'${rds.route.shortestName}'|D:${rds.direction.headsignValue}] trip update: ${gTripUpdate.toStringExt()}." ) } } @@ -130,21 +134,21 @@ object GTFSRealTimeTripUpdatesProvider { sortedRDS = context.getRDS(rds.authority, routeId, directionId) } if (uuidSchedule == null) { - uuidSchedule = - sortedRDS - ?.let { rdsList -> - context - .getRDSSchedule(targetAuthority, rdsList) - .associateBy { it.targetUUID } - } + uuidSchedule = sortedRDS + ?.let { rdsList -> + context + .getRDSSchedule(targetAuthority, rdsList) + .associateBy { it.targetUUID } + } } uuidSchedule ?: return null sortedRDS ?: return null processRDTripUpdates(rdTripUpdates, uuidSchedule, sortedRDS) uuidSchedule.values.filterNotNull().forEach { schedule -> if (!schedule.timestamps.any { it.isRealTime }) return@forEach - schedule.sourceLabel = "GTFS-RT" // FIXME + schedule.sourceLabel = sourceLabel schedule.readFromSourceAtInMs = readFromSourceMs + schedule.providerPrecisionInMs = PROVIDER_PRECISION_IN_MS cacheStatus(schedule) } return getCachedStatusS(filter.targetUUID, tripIds) @@ -153,6 +157,7 @@ object GTFSRealTimeTripUpdatesProvider { return null } } + @JvmStatic fun GTFSRealTimeProvider.getNew(statusFilter: StatusProviderContract.Filter): POIStatus? { val filter = statusFilter as? Schedule.ScheduleStatusFilter ?: run { @@ -236,7 +241,7 @@ object GTFSRealTimeTripUpdatesProvider { } catch (e: Exception) { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, e, "loadAgencyDataFromWWW() > error while parsing GTFS Real Time data!") } - MTLog.i(this@GTFSRealTimeTripUpdatesProvider, "Found %d vehicle locations.", statuses.size) + MTLog.i(this@GTFSRealTimeTripUpdatesProvider, "Found %d trip updates.", statuses.size) if (Constants.DEBUG) { for (schedule in statuses) { MTLog.d(this@GTFSRealTimeTripUpdatesProvider, "loadAgencyDataFromWWW() > - new $schedule.") From 5bcefdbb874b3503f19e5602c013a44a429162d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 11:45:57 -0400 Subject: [PATCH 26/39] fix --- .../status/GTFSRealTimeTripUpdatesProvider.kt | 6 ++- .../GTFSRealTimeTripUpdatesProviderExt.kt | 46 +++++++++---------- 2 files changed, 27 insertions(+), 25 deletions(-) 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 a75056c6..50ec001a 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 @@ -36,7 +36,11 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import com.google.transit.realtime.GtfsRealtime.FeedMessage as GFeedMessage -object GTFSRealTimeTripUpdatesProvider { +object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { + + internal val LOG_TAG: String = GTFSRealTimeTripUpdatesProvider::class.java.simpleName + + override fun getLogTag() = LOG_TAG val PROVIDER_PRECISION_IN_MS = 10.seconds.inWholeMilliseconds 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 c0053a5a..558f08fe 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 @@ -1,5 +1,6 @@ package org.mtransit.android.commons.provider.status +import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.TimeUtilsK import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule @@ -17,8 +18,10 @@ import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpd import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeInstant import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt import org.mtransit.android.commons.provider.gtfs.parseStopId import org.mtransit.android.commons.provider.gtfs.parseTripId +import org.mtransit.android.commons.provider.status.GTFSRealTimeTripUpdatesProvider.LOG_TAG import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant @@ -69,7 +72,7 @@ internal fun makeTargetUuidAndSequenceList( .sortedBy { (_, stopSequence) -> stopSequence } } var generatedStopSequence = 1 - return buildList { + return buildSet { // unicity of uuid+sequence tripTargetUuidSchedule.forEach { (targetUuid, schedule) -> schedule?.timestamps?.filter { it.tripId == tripId }?.forEach { timestamp -> val stopSequence = timestamp.stopSequenceOrNull ?: generatedStopSequence @@ -98,6 +101,10 @@ internal fun processRDTripUpdate( } } } + if (gTripUpdate.optDelay == null && gTripUpdate.stopTimeUpdateCount == 0) { + MTLog.d(LOG_TAG, "processRDTripUpdate() > SKIP (useless trip update: ${gTripUpdate.toStringExt()})") + return // nothing to do + } var stuIdx = 0 var uuidAndSeqIdx = 0 var currentDelay = gTripUpdate.optDelay?.seconds // initial delay valid until 1st stop time update @@ -125,18 +132,11 @@ internal fun processRDTripUpdate( } } -internal fun applyDelaySTU( - tripId: String, - stopSequence: Int, - rdsSchedule: Schedule?, - gStopTimeUpdate: GTUStopTimeUpdate, - currentDelay: Duration? = null, -): Duration? { - val rdsTripTimestamp = rdsSchedule - ?.timestamps?.filter { it.tripId == tripId } - ?.filter { timestamp -> +private fun Schedule.findClosestTripTimestamp(tripId: String, stopSequence: Int) = + timestamps.filter { it.tripId == tripId } + .filter { timestamp -> (timestamp.stopSequenceOrNull == null || timestamp.stopSequenceOrNull == stopSequence) - }?.let { rdsTripTimestamps -> + }.let { rdsTripTimestamps -> if (rdsTripTimestamps.size > 1) { val now = TimeUtilsK.currentInstant() rdsTripTimestamps.sortedBy { (it.departure - now).absoluteValue } @@ -144,6 +144,15 @@ internal fun applyDelaySTU( rdsTripTimestamps }.firstOrNull() } + +internal fun applyDelaySTU( + tripId: String, + stopSequence: Int, + rdsSchedule: Schedule?, + gStopTimeUpdate: GTUStopTimeUpdate, + currentDelay: Duration? = null, +): Duration? { + val rdsTripTimestamp = rdsSchedule?.findClosestTripTimestamp(tripId, stopSequence) ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure @@ -186,18 +195,7 @@ internal fun applyDelay( currentDelay: Duration? ): Duration? { currentDelay ?: return null - val rdsTripTimestamp = rdsSchedule - ?.timestamps?.filter { it.tripId == tripId } - ?.filter { timestamp -> - (timestamp.stopSequenceOrNull == null || timestamp.stopSequenceOrNull == stopSequence) - }?.let { rdsTripTimestamps -> - if (rdsTripTimestamps.size > 1) { - val now = TimeUtilsK.currentInstant() - rdsTripTimestamps.sortedBy { (it.departure - now).absoluteValue } - } else { - rdsTripTimestamps - }.firstOrNull() - } + val rdsTripTimestamp = rdsSchedule?.findClosestTripTimestamp(tripId, stopSequence) ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.departure - rdsTripTimestamp.arrival if (currentDelay < Duration.ZERO) { From 39a633037ca865d1a60453e2f79e90b5c1422c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 13:28:42 -0400 Subject: [PATCH 27/39] cleanup --- .../status/GTFSRealTimeTripUpdatesProviderExt.kt | 10 +++++----- .../status/GTFSRealTimeTripUpdatesProviderTests.kt | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) 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 558f08fe..79e1c24c 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 @@ -176,14 +176,14 @@ internal fun applyDelaySTU( internal fun GTUStopTimeEvent?.makeDelay( originalTime: Instant, - previousDelay: Duration? = null, - previousOriginalDiff: Duration? = null, + previousSTEDelay: Duration? = null, + previousCurrentDiff: Duration? = null, ): Duration? { return this?.optDelay?.seconds ?: this?.optTimeInstant?.let { time -> time - originalTime } - ?: previousDelay?.let { - previousOriginalDiff?.let { - (previousDelay - previousOriginalDiff).coerceAtLeast(Duration.ZERO) + ?: previousSTEDelay?.let { + previousCurrentDiff?.let { + (previousSTEDelay - previousCurrentDiff).coerceAtLeast(Duration.ZERO) } } } 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 2ff22ed1..21638733 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 @@ -271,19 +271,19 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_makeDelay_3() { val departure = DEPARTURE_MS.secsToInstant() - val arrival = departure - 5.minutes + val arrival = departure - 3.minutes val timestamp = mkTime(departure, arrival = arrival) val previousDelay = 10.minutes val stopTimeEvent: GTUStopTimeEvent? = null val result = stopTimeEvent.makeDelay( originalTime = timestamp.departure, - previousDelay = previousDelay, - previousOriginalDiff = timestamp.arrivalDiff + previousSTEDelay = previousDelay, + previousCurrentDiff = timestamp.arrivalDiff ) assertNotNull(result) - assertEquals(5.minutes, result) + assertEquals(7.minutes, result) } // endregion From 78a15bb40c6a14885d744b61287ac65cb810b5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 13:32:47 -0400 Subject: [PATCH 28/39] cleanup --- .../java/org/mtransit/android/commons/data/ScheduleExt.kt | 3 +-- .../provider/status/GTFSRealTimeTripUpdatesProviderExt.kt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 17f109a8..68e8f624 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -19,8 +19,7 @@ var Schedule.Timestamp.departure: Instant departureT = value.toMillis() } -@Suppress("unused") -val Schedule.Timestamp.arrivalDiff: Duration? get() = this.arrivalDiffMs?.milliseconds?.coerceAtLeast(Duration.ZERO) +val Schedule.Timestamp.arrivalDiff get() = this.arrivalDiffMs?.milliseconds?.coerceAtLeast(Duration.ZERO) ?: Duration.ZERO var Schedule.Timestamp.arrival: Instant get() = arrivalT.millisToInstant() 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 79e1c24c..0e621b28 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 @@ -156,7 +156,7 @@ internal fun applyDelaySTU( ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure - val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff ?: Duration.ZERO + val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff val stuArrivalDelay = gStopTimeUpdate.optArrival .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .makeDelay(timestampOriginalArrival) @@ -197,7 +197,7 @@ internal fun applyDelay( currentDelay ?: return null val rdsTripTimestamp = rdsSchedule?.findClosestTripTimestamp(tripId, stopSequence) ?: return currentDelay - val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.departure - rdsTripTimestamp.arrival + val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.arrivalDiff if (currentDelay < Duration.ZERO) { rdsTripTimestamp.arrival += currentDelay rdsTripTimestamp.departure += currentDelay From d9a0d682e1306aaa7cbf55c1a4db17661752e91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 13:34:10 -0400 Subject: [PATCH 29/39] cleanup --- .../commons/provider/status/StatusProviderExt.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt b/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt index c11df2ae..4b506fe0 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/StatusProviderExt.kt @@ -20,18 +20,6 @@ fun

P.getCachedStatusS( this.contentUri, buildString { append(SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_UUID, targetUUIDs)) - // TODO ? if (FeatureFlags.F_USE_TRIP_IS_FOR_STATUSES) { - // tripIds?.takeIf { it.isNotEmpty() }?.let { - // append(SqlUtils.AND) - // append( - // SqlUtils.getWhereGroup( - // SqlUtils.OR, - // SqlUtils.getWhereInString(StatusProviderContract.Columns.T_STATUS_K_TARGET_TRIP_ID, it), - // SqlUtils.getWhereColumnIsNull(StatusProviderContract.Columns.T_STATUS_K_TARGET_TRIP_ID), - // ) - // ) - // } - // } } ) } From c01914933c4f92c1293b2a02f82f2e315fb76d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 13:41:34 -0400 Subject: [PATCH 30/39] unit test++ --- .../status/GTFSRealTimeTripUpdatesProviderTests.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 21638733..2e2e3c37 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 @@ -349,7 +349,6 @@ class GTFSRealTimeTripUpdatesProviderTests { trip = tripDescriptor { tripId = TRIP_ID } - delayDuration = 1.minutes stopTimeUpdate += stopTimeUpdate { stopId = "2000" departure = stopTimeEvent { @@ -405,14 +404,14 @@ class GTFSRealTimeTripUpdatesProviderTests { assertNotNull(tripTargetUuidSchedule[rdsList[0].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> - assertEquals(startsAt + 1.minutes, timestamp.arrival) - assertEquals(startsAt + 1.minutes, timestamp.departure) - assertTrue { timestamp.isRealTime } + assertEquals(startsAt, timestamp.arrival) + assertEquals(startsAt, timestamp.departure) + assertFalse { timestamp.isRealTime } } } assertNotNull(tripTargetUuidSchedule[rdsList[1].uuid]) { schedule -> assertNotNull(schedule.timestamps.singleOrNull()) { timestamp -> - assertEquals(startsAt + 10.minutes + 1.minutes, timestamp.arrival) + assertEquals(startsAt + 10.minutes, timestamp.arrival) assertEquals(startsAt + 10.minutes + 3.minutes, timestamp.departure) assertTrue { timestamp.isRealTime } } From f35f5dcdeae590d4e884da2998eb985bfed6e2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 13:49:36 -0400 Subject: [PATCH 31/39] PR comments --- .../android/commons/data/Schedule.java | 11 ++-- .../status/GTFSRealTimeTripUpdatesProvider.kt | 31 ++++------ .../GTFSRealTimeTripUpdatesProviderTests.kt | 60 +++++++++---------- 3 files changed, 47 insertions(+), 55 deletions(-) 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 2697d73d..7ca5006c 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -301,9 +301,10 @@ public boolean isUseful() { private static class TimestampComparator implements Comparator { @Override public int compare(Timestamp lhs, Timestamp rhs) { - long lt = lhs == null ? 0L : lhs.getDepartureT(); - long rt = rhs == null ? 0L : rhs.getDepartureT(); - return (int) (lt - rt); + return Long.compare( + lhs == null ? 0L : lhs.getDepartureT(), + rhs == null ? 0L : rhs.getDepartureT() + ); } } @@ -311,9 +312,9 @@ private static class FrequencyComparator implements Comparator { @Override public int compare(Frequency lhs, Frequency rhs) { if (lhs.startTimeInMs == rhs.startTimeInMs) { - return (int) (lhs.endTimeInMs - rhs.endTimeInMs); + return Long.compare(lhs.endTimeInMs, rhs.endTimeInMs); } - return (int) (lhs.startTimeInMs - rhs.startTimeInMs); + return Long.compare(lhs.startTimeInMs, rhs.startTimeInMs); } } 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 50ec001a..b1656e6e 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 @@ -22,7 +22,6 @@ import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.makeRequest import org.mtransit.android.commons.provider.gtfs.parseRouteId import org.mtransit.android.commons.provider.gtfs.parseTripId -import org.mtransit.android.commons.provider.status.StatusProvider.cacheAllStatusesBulkLockDB import org.mtransit.commons.SourceUtils import java.io.File import java.io.IOException @@ -211,47 +210,39 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { deleteAllCachedStatus() deleteAllDone = true } - val newStatuses = loadAgencyDataFromWWW(context) - if (newStatuses != null) { // empty is OK + val newStatusesLoaded = loadAgencyDataFromWWW(context) + if (newStatusesLoaded) { // empty is OK if (!deleteAllDone) { deleteAllCachedStatus() } - cacheAllStatusesBulkLockDB(this, newStatuses) + // no caching, will make as requested from cached file } // else keep whatever we have until max validity reached } private const val GTFS_RT_TRIP_UPDATE_PB_FILE_NAME = "gtfs_rt_trip_update.pb" - private fun GTFSRealTimeProvider.loadAgencyDataFromWWW(context: Context): List? { + private fun GTFSRealTimeProvider.loadAgencyDataFromWWW(context: Context): Boolean { try { val urlRequest = makeRequest( context, urlCachedString = GTFSRealTimeProvider.getAGENCY_TRIP_UPDATES_URL_CACHED(context), getUrlString = { token -> GTFSRealTimeProvider.getAgencyTripUpdatesUrlString(context, token) } - ) ?: return null + ) ?: return false getOkHttpClient(context).newCall(urlRequest).execute().use { response -> GtfsRealTimeStorage.saveTripUpdateLastUpdateCode(context, response.code) GtfsRealTimeStorage.saveTripUpdateLastUpdateMs(context, TimeUtils.currentTimeMillis()) when (response.code) { HttpURLConnection.HTTP_OK -> { - val statuses = mutableListOf() try { try { File(context.cacheDir, GTFS_RT_TRIP_UPDATE_PB_FILE_NAME).writeBytes(response.body.bytes()) } catch (e: IOException) { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, e, "loadAgencyDataFromWWW() > error while saving GTFS RT Trip Updates data!") } - return null } catch (e: Exception) { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, e, "loadAgencyDataFromWWW() > error while parsing GTFS Real Time data!") } - MTLog.i(this@GTFSRealTimeTripUpdatesProvider, "Found %d trip updates.", statuses.size) - if (Constants.DEBUG) { - for (schedule in statuses) { - MTLog.d(this@GTFSRealTimeTripUpdatesProvider, "loadAgencyDataFromWWW() > - new $schedule.") - } - } - return statuses + return true } else -> { @@ -259,7 +250,7 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { this@GTFSRealTimeTripUpdatesProvider, "ERROR: HTTP URL-Connection Response Code ${response.code} (Message: ${response.message})" ) - return null + return false } } } @@ -268,20 +259,20 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { SecurityUtils.logCertPathValidatorException(sslhe) GtfsRealTimeStorage.saveTripUpdateLastUpdateCode(context, 567) // SSL certificate not trusted (on this device) GtfsRealTimeStorage.saveTripUpdateLastUpdateMs(context, TimeUtils.currentTimeMillis()) - return null + return false } catch (uhe: UnknownHostException) { if (MTLog.isLoggable(Log.DEBUG)) { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, uhe, "No Internet Connection!") } else { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, "No Internet Connection!") } - return null + return false } catch (se: SocketException) { MTLog.w(this@GTFSRealTimeTripUpdatesProvider, se, "No Internet Connection!") - return null + return false } catch (e: Exception) { // Unknown error MTLog.e(this@GTFSRealTimeTripUpdatesProvider, e, "INTERNAL ERROR: Unknown Exception") - return null + return false } } } 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 2e2e3c37..8b9ae6d9 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 @@ -37,7 +37,7 @@ class GTFSRealTimeTripUpdatesProviderTests { companion object { private const val LOCAL_TZ_ID: String = "America/Montreal" - private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: + private val DEPARTURE = 1772722800L.secsToInstant() // 2026-03-06 10:00: private const val NOW_IN_MS = 123456789_000L @@ -63,8 +63,8 @@ class GTFSRealTimeTripUpdatesProviderTests { // region applyDelay @Test - fun text_applyDelay_null() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelay_null() { + val departure = DEPARTURE val timestamp = mkTime(departure) val delay: Duration? = null @@ -76,8 +76,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelay_0_on_time() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelay_0_on_time() { + val departure = DEPARTURE val timestamp = mkTime(departure) val delay = Duration.ZERO @@ -90,8 +90,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelay_simple_late() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelay_simple_late() { + val departure = DEPARTURE val timestamp = mkTime(departure) val delay = 10.minutes @@ -104,8 +104,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelay_differentArrival_late() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelay_differentArrival_late() { + val departure = DEPARTURE val arrival = departure - 1.minutes val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes @@ -120,8 +120,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelay_consumed_late() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelay_consumed_late() { + val departure = DEPARTURE val arrival = departure - 15.minutes val timestamp = mkTime(departure, arrival = arrival) val delay = 10.minutes @@ -136,8 +136,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelay_simple_early() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelay_simple_early() { + val departure = DEPARTURE val arrival = departure - 1.minutes val timestamp = mkTime(departure, arrival = arrival) val delay = (-5).minutes @@ -156,8 +156,8 @@ class GTFSRealTimeTripUpdatesProviderTests { // region applyDelaySTU @Test - fun text_applyDelaySTU_simple() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelaySTU_simple() { + val departure = DEPARTURE val timestamp = mkTime(departure) val stopTimeUpdate = stopTimeUpdate { this.departure = stopTimeEvent { @@ -174,8 +174,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelaySTU_2() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelaySTU_2() { + val departure = DEPARTURE val arrival = departure - 5.minutes val timestamp = mkTime(departure, arrival = arrival) val stopTimeUpdate = stopTimeUpdate { @@ -197,8 +197,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelaySTU_3() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelaySTU_3() { + val departure = DEPARTURE val arrival = departure - 5.minutes val delay = 1.minutes val timestamp = mkTime(departure, arrival = arrival) @@ -218,8 +218,8 @@ class GTFSRealTimeTripUpdatesProviderTests { } @Test - fun text_applyDelaySTU_4() { - val departure = DEPARTURE_MS.secsToInstant() + fun test_applyDelaySTU_4() { + val departure = DEPARTURE val arrival = departure - 1.minutes val delay = 15.minutes // should be ignored val timestamp = mkTime(departure, arrival = arrival) @@ -244,7 +244,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_makeDelay_1() { - val originalTime = DEPARTURE_MS.secsToInstant() + val originalTime = DEPARTURE val stopTimeEvent = stopTimeEvent { delay = 10 } @@ -257,7 +257,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_makeDelay_2() { - val originalTime = DEPARTURE_MS.secsToInstant() + val originalTime = DEPARTURE val stopTimeEvent = stopTimeEvent { time = (originalTime + 10.seconds).toSecs() } @@ -270,7 +270,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_makeDelay_3() { - val departure = DEPARTURE_MS.secsToInstant() + val departure = DEPARTURE val arrival = departure - 3.minutes val timestamp = mkTime(departure, arrival = arrival) val previousDelay = 10.minutes @@ -297,7 +297,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_processRDTripUpdate_singleTUDelay() { - val tripStart = DEPARTURE_MS.secsToInstant() + val tripStart = DEPARTURE val gTripUpdate = tripUpdate { trip = tripDescriptor { this.tripId = tripId @@ -344,7 +344,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_processRDTripUpdate_combined_complex_stop_id() { - val startsAt = DEPARTURE_MS.secsToInstant() + val startsAt = DEPARTURE val gTripUpdate = tripUpdate { trip = tripDescriptor { tripId = TRIP_ID @@ -467,7 +467,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_processRDTripUpdate_combined_complex_stop_sequence() { - val startsAt = DEPARTURE_MS.secsToInstant() + val startsAt = DEPARTURE val gTripUpdate = tripUpdate { trip = tripDescriptor { tripId = TRIP_ID @@ -594,7 +594,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_processRDTripUpdate_combined_complex_stop_sequence_repeated_stop() { - val startsAt = DEPARTURE_MS.secsToInstant() + val startsAt = DEPARTURE val gTripUpdate = tripUpdate { trip = tripDescriptor { tripId = TRIP_ID @@ -724,7 +724,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_processRDTripUpdate_trip_cancelled() { - val startsAt = DEPARTURE_MS.secsToInstant() + val startsAt = DEPARTURE val gTripUpdate = tripUpdate { trip = tripDescriptor { tripId = TRIP_ID @@ -763,7 +763,7 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_processRDTripUpdate_trip_deleted() { - val startsAt = DEPARTURE_MS.secsToInstant() + val startsAt = DEPARTURE val gTripUpdate = tripUpdate { trip = tripDescriptor { tripId = TRIP_ID From 991fede93822a22a78c82a3f34e408bdf856cd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 14:40:42 -0400 Subject: [PATCH 32/39] improvements --- .../org/mtransit/android/commons/data/POIStatus.java | 6 +++++- .../status/GTFSRealTimeTripUpdatesProvider.kt | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/android/commons/data/POIStatus.java b/src/main/java/org/mtransit/android/commons/data/POIStatus.java index 43ea7ce2..902207a4 100644 --- a/src/main/java/org/mtransit/android/commons/data/POIStatus.java +++ b/src/main/java/org/mtransit/android/commons/data/POIStatus.java @@ -47,7 +47,7 @@ protected static int getDefaultStatusTextColor(@NonNull Context context) { @ItemStatusType private final int type; private final long lastUpdateInMs; - private final long maxValidityInMs; + private long maxValidityInMs; private long readFromSourceAtInMs; @Nullable private String sourceLabel; @@ -233,6 +233,10 @@ public long getMaxValidityInMs() { return maxValidityInMs; } + public void setMaxValidityInMs(long maxValidityInMs) { + this.maxValidityInMs = maxValidityInMs; + } + public long getReadFromSourceAtInMs() { return readFromSourceAtInMs; } 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 b1656e6e..92f90831 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 @@ -6,9 +6,11 @@ import org.mtransit.android.commons.Constants import org.mtransit.android.commons.MTLog import org.mtransit.android.commons.SecurityUtils import org.mtransit.android.commons.TimeUtils +import org.mtransit.android.commons.TimeUtilsK import org.mtransit.android.commons.data.POIStatus import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isIGNORE_DIRECTION import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage @@ -149,9 +151,18 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { processRDTripUpdates(rdTripUpdates, uuidSchedule, sortedRDS) uuidSchedule.values.filterNotNull().forEach { schedule -> if (!schedule.timestamps.any { it.isRealTime }) return@forEach + val now = TimeUtilsK.currentInstant() + val minDateForRealTime = now - 10.minutes + val maxDateForRealTime = now + 12.hours + schedule.timestamps.filter { + it.departure !in minDateForRealTime..maxDateForRealTime + }.forEach { timestamp -> + schedule.removeTimestamp(timestamp) + } schedule.sourceLabel = sourceLabel schedule.readFromSourceAtInMs = readFromSourceMs schedule.providerPrecisionInMs = PROVIDER_PRECISION_IN_MS + schedule.maxValidityInMs = maxValidityInMs cacheStatus(schedule) } return getCachedStatusS(filter.targetUUID, tripIds) From b2c378c5496d1cdb2b76ce2c3fe956f1ac71af32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 11 Mar 2026 15:59:27 -0400 Subject: [PATCH 33/39] wip --- .../android/commons/data/POIStatus.java | 6 ++++- .../status/GTFSRealTimeTripUpdatesProvider.kt | 27 ++++++++++++++----- .../GTFSRealTimeTripUpdatesProviderExt.kt | 13 +++++++-- .../GTFSRealTimeTripUpdatesProviderTests.kt | 26 ++++++++++++++++++ 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/POIStatus.java b/src/main/java/org/mtransit/android/commons/data/POIStatus.java index 902207a4..eb337a37 100644 --- a/src/main/java/org/mtransit/android/commons/data/POIStatus.java +++ b/src/main/java/org/mtransit/android/commons/data/POIStatus.java @@ -46,7 +46,7 @@ protected static int getDefaultStatusTextColor(@NonNull Context context) { private String targetUUID; @ItemStatusType private final int type; - private final long lastUpdateInMs; + private long lastUpdateInMs; private long maxValidityInMs; private long readFromSourceAtInMs; @Nullable @@ -229,6 +229,10 @@ public long getLastUpdateInMs() { return lastUpdateInMs; } + public void setLastUpdateInMs(long lastUpdateInMs) { + this.lastUpdateInMs = lastUpdateInMs; + } + public long getMaxValidityInMs() { return maxValidityInMs; } 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 92f90831..6f1088b9 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 @@ -10,6 +10,7 @@ import org.mtransit.android.commons.TimeUtilsK import org.mtransit.android.commons.data.POIStatus import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule +import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isIGNORE_DIRECTION @@ -152,14 +153,26 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { uuidSchedule.values.filterNotNull().forEach { schedule -> if (!schedule.timestamps.any { it.isRealTime }) return@forEach val now = TimeUtilsK.currentInstant() - val minDateForRealTime = now - 10.minutes - val maxDateForRealTime = now + 12.hours - schedule.timestamps.filter { - it.departure !in minDateForRealTime..maxDateForRealTime - }.forEach { timestamp -> - schedule.removeTimestamp(timestamp) - } + var oldestDateForRealTime = now - 1.minutes + var maxFutureDateForRealTime = now + 12.hours + val (past, future) = schedule.timestamps.partition { it.departure < now } + oldestDateForRealTime = past.filter { it.isRealTime }.minOfOrNull { it.arrival } // all real-time + ?: oldestDateForRealTime + maxFutureDateForRealTime = future.take(10).maxOfOrNull { it.departure } // keep firsts 10 + ?.takeIf { it > maxFutureDateForRealTime } + ?: maxFutureDateForRealTime + maxFutureDateForRealTime = future.filter { it.isRealTime }.maxOfOrNull { it.departure } // all real-time + ?.takeIf { it > maxFutureDateForRealTime } + ?: maxFutureDateForRealTime + schedule.timestamps + .filterNot { + it.isRealTime || oldestDateForRealTime < it.arrival && it.departure < maxFutureDateForRealTime + } + .forEach { timestamp -> + schedule.removeTimestamp(timestamp) + } schedule.sourceLabel = sourceLabel + schedule.lastUpdateInMs = readFromSourceMs schedule.readFromSourceAtInMs = readFromSourceMs schedule.providerPrecisionInMs = PROVIDER_PRECISION_IN_MS schedule.maxValidityInMs = maxValidityInMs 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 0e621b28..ddd3055f 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 @@ -100,6 +100,7 @@ internal fun processRDTripUpdate( schedule.removeTimestamp(it) } } + return } if (gTripUpdate.optDelay == null && gTripUpdate.stopTimeUpdateCount == 0) { MTLog.d(LOG_TAG, "processRDTripUpdate() > SKIP (useless trip update: ${gTripUpdate.toStringExt()})") @@ -157,16 +158,24 @@ internal fun applyDelaySTU( val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure val timestampOriginalArrivalDiff = rdsTripTimestamp.arrivalDiff + val stuArrivalTime = gStopTimeUpdate.optArrival + .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } + ?.optTimeInstant val stuArrivalDelay = gStopTimeUpdate.optArrival .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .makeDelay(timestampOriginalArrival) ?: currentDelay .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } + val stuDepartureTime = gStopTimeUpdate.optDeparture + .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } + ?.optTimeInstant val stuDepartureDelay = gStopTimeUpdate.optDeparture .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .makeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) - stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } - stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } + stuArrivalTime?.let { rdsTripTimestamp.arrival = it; rdsTripTimestamp.realTime = true } + ?: stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } + stuDepartureTime?.let { rdsTripTimestamp.departure = it; rdsTripTimestamp.realTime = true } + ?: stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } if (gStopTimeUpdate.scheduleRelationship == GTUSTUScheduleRelationship.SKIPPED) { rdsSchedule.removeTimestamp(rdsTripTimestamp) } 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 8b9ae6d9..7b59442b 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 @@ -173,6 +173,32 @@ class GTFSRealTimeTripUpdatesProviderTests { assertEquals(departure + 1.minutes, timestamp.departure) } + @Test + fun test_applyDelaySTU_preferTimeOverDelay() { + val departure = DEPARTURE + val arrival = departure - 5.minutes + // val updatedArrival = + val timestamp = mkTime(departure, arrival = arrival) + val stopTimeUpdate = stopTimeUpdate { + this.arrival = stopTimeEvent { + delayDuration = (-1).minutes + time = (arrival - 63.seconds).toSecs() + } + this.departure = stopTimeEvent { + delayDuration = 2.minutes + time = (departure + 124.seconds).toSecs() + } + } + + val result = applyDelaySTU(TRIP_ID, STOP_SEQUENCE, timestamp.toSchedule(), stopTimeUpdate) + + assertNotNull(result) + assertEquals(2.minutes, result) + assertTrue { timestamp.isRealTime } + assertEquals(arrival - 63.seconds, timestamp.arrival) + assertEquals(departure + 124.seconds, timestamp.departure) + } + @Test fun test_applyDelaySTU_2() { val departure = DEPARTURE From b93ae5e49d7c945443f592ad86a446e4c63047c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 12 Mar 2026 09:45:09 -0400 Subject: [PATCH 34/39] Add original departure/arrival delay --- .../android/commons/data/Schedule.java | 46 +++++++++++++- .../android/commons/data/ScheduleExt.kt | 63 ++++++++++++++++--- .../GTFSRealTimeTripUpdatesProviderExt.kt | 22 +++---- .../android/commons/data/ScheduleExtTests.kt | 54 +++++++++++++++- 4 files changed, 164 insertions(+), 21 deletions(-) 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 7ca5006c..631346a6 100644 --- a/src/main/java/org/mtransit/android/commons/data/Schedule.java +++ b/src/main/java/org/mtransit/android/commons/data/Schedule.java @@ -438,6 +438,7 @@ public String getLogTag() { } private long departureInMs; + private long originalDepartureDelayMs = 0L; @Direction.HeadSignType private int headsignType = Direction.HEADSIGN_TYPE_NONE; @Nullable @@ -455,6 +456,7 @@ public String getLogTag() { private int stopSequence = -1; @Nullable private Long arrivalDiffMs = null; + private long originalArrivalDelayMs = 0L; @VisibleForTesting public Timestamp(long departureT) { @@ -480,6 +482,14 @@ public void setDepartureT(long departureT) { setArrivalT(originalArrivalT); // stored as diff -> do not change } + public long getOriginalDepartureDelayMs() { + return originalDepartureDelayMs; + } + + public void setOriginalDepartureDelayMs(long originalDepartureDelayMs) { + this.originalDepartureDelayMs = originalDepartureDelayMs; + } + public long getArrivalT() { return getDepartureT() - (arrivalDiffMs == null ? 0L : arrivalDiffMs); } @@ -502,6 +512,14 @@ public Long getArrivalDiffMs() { return arrivalDiffMs; } + public long getOriginalArrivalDelayMs() { + return originalArrivalDelayMs; + } + + public void setOriginalArrivalDelayMs(long originalArrivalDelayMs) { + this.originalArrivalDelayMs = originalArrivalDelayMs; + } + @NonNull public Timestamp setHeadsign(@Direction.HeadSignType int headsignType, @Nullable String headsignValue) { this.headsignType = headsignType; @@ -676,6 +694,7 @@ public boolean equals(Object o) { Timestamp timestamp = (Timestamp) o; if (departureInMs != timestamp.departureInMs) return false; + if (originalDepartureDelayMs != timestamp.originalDepartureDelayMs) return false; if (headsignType != timestamp.headsignType) return false; if (!Objects.equals(headsignValue, timestamp.headsignValue)) return false; if (!Objects.equals(localTimeZoneId, timestamp.localTimeZoneId)) return false; @@ -685,6 +704,7 @@ public boolean equals(Object o) { if (!Objects.equals(tripId, timestamp.tripId)) return false; if (stopSequence != timestamp.stopSequence) return false; if (!Objects.equals(arrivalDiffMs, timestamp.arrivalDiffMs)) return false; + if (originalArrivalDelayMs != timestamp.originalArrivalDelayMs) return false; // if (!Objects.equals(heading, timestamp.heading)) return false; // LAZY return true; } @@ -692,6 +712,7 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = Long.hashCode(departureInMs); + result = 31 * result + Long.hashCode(originalDepartureDelayMs); result = 31 * result + headsignType; result = 31 * result + (headsignValue != null ? headsignValue.hashCode() : 0); result = 31 * result + (localTimeZoneId != null ? localTimeZoneId.hashCode() : 0); @@ -701,6 +722,7 @@ public int hashCode() { result = 31 * result + (tripId != null ? tripId.hashCode() : 0); result = 31 * result + stopSequence; result = 31 * result + (arrivalDiffMs != null ? arrivalDiffMs.hashCode() : 0); + result = 31 * result + Long.hashCode(originalArrivalDelayMs); // result = 31 * result + (heading != null ? heading.hashCode() : 0); // LAZY return result; } @@ -710,10 +732,16 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(Timestamp.class.getSimpleName()); sb.append('{'); - sb.append("t=").append(Constants.DEBUG ? MTLog.formatDateTime(getDepartureT()) : getDepartureT()); + sb.append("d=").append(Constants.DEBUG ? MTLog.formatDateTime(getDepartureT()) : getDepartureT()); + if (this.originalDepartureDelayMs != 0L) { + sb.append(", oDd:").append(this.originalDepartureDelayMs); + } if (arrivalDiffMs != null) { sb.append(", aD:").append(arrivalDiffMs); } + if (this.originalArrivalDelayMs != 0L) { + sb.append(", oAd:").append(this.originalArrivalDelayMs); + } if (tripId != null) { sb.append(", tripId:'").append(tripId).append('\''); } @@ -743,7 +771,9 @@ public String toString() { } private static final String JSON_DEPARTURE = "t"; + private static final String JSON_ORIGINAL_DEPARTURE_DELAY = "tOD"; private static final String JSON_ARRIVAL_DIFF = "tDiffA"; + private static final String JSON_ORIGINAL_ARRIVAL_DELAY = "tOA"; private static final String JSON_TRIP_ID = "trip_id"; private static final String JSON_STOP_SEQUENCE = "stop_seq"; private static final String JSON_HEADSIGN_TYPE = "ht"; @@ -758,9 +788,17 @@ static Timestamp parseJSON(@NonNull JSONObject jTimestamp) { try { final long departureInMs = jTimestamp.getLong(JSON_DEPARTURE); final Timestamp timestamp = new Timestamp(departureInMs); + final long originalDepartureDelayMs = jTimestamp.optLong(JSON_ORIGINAL_DEPARTURE_DELAY, 0L); + if (originalDepartureDelayMs != 0L) { + timestamp.setOriginalDepartureDelayMs(originalDepartureDelayMs); + } if (jTimestamp.has(JSON_ARRIVAL_DIFF)) { timestamp.setArrivalDiffMs(jTimestamp.getLong(JSON_ARRIVAL_DIFF)); } + final long originalArrivalDelayMs = jTimestamp.optLong(JSON_ORIGINAL_ARRIVAL_DELAY, 0L); + if (originalArrivalDelayMs != 0L) { + timestamp.setOriginalArrivalDelayMs(originalArrivalDelayMs); + } if (jTimestamp.has(JSON_TRIP_ID)) { timestamp.setTripId(jTimestamp.getString(JSON_TRIP_ID)); } @@ -806,9 +844,15 @@ public static JSONObject toJSON(@NonNull Timestamp timestamp) { try { JSONObject jTimestamp = new JSONObject(); jTimestamp.put(JSON_DEPARTURE, timestamp.departureInMs); + if (timestamp.originalDepartureDelayMs != 0L) { + jTimestamp.put(JSON_ORIGINAL_DEPARTURE_DELAY, timestamp.originalDepartureDelayMs); + } if (timestamp.arrivalDiffMs != null) { jTimestamp.put(JSON_ARRIVAL_DIFF, timestamp.arrivalDiffMs); } + if (timestamp.originalArrivalDelayMs != 0L) { + jTimestamp.put(JSON_ORIGINAL_ARRIVAL_DELAY, timestamp.originalArrivalDelayMs); + } if (timestamp.tripId != null) { jTimestamp.put(JSON_TRIP_ID, timestamp.tripId); } diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 68e8f624..1920817e 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -4,25 +4,74 @@ import org.mtransit.android.commons.millisToInstant import org.mtransit.android.commons.toMillis import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null, stopSequence: Int? = null) = Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { - arrival?.let { this.arrival = it } + arrival?.let { this.arrivalT = it.toMillis() } tripId?.let { this.tripId = it } stopSequence?.let { this.setStopSequence(it) } } -var Schedule.Timestamp.departure: Instant - get() = departureT.millisToInstant() +fun Schedule.Timestamp.isDepartureLate(minDelay: Duration = 30.seconds) = + originalDepartureDelay > minDelay + +fun Schedule.Timestamp.isDepartureEarly(maxDelay: Duration = 30.seconds) = + originalDepartureDelay < -maxDelay + +fun Schedule.Timestamp.isArrivalLate(minDelay: Duration = 30.seconds) = + originalArrivalDelay > minDelay + +fun Schedule.Timestamp.isArrivalEarly(maxDelay: Duration = 30.seconds) = + originalArrivalDelay < -maxDelay + +val Schedule.Timestamp.departure get() = departureT.millisToInstant() + +var Schedule.Timestamp.originalDepartureDelay: Duration + get() = originalDepartureDelayMs.milliseconds set(value) { - departureT = value.toMillis() + originalDepartureDelayMs = value.inWholeMilliseconds } +val Schedule.Timestamp.originalDeparture get() = departure - originalDepartureDelay + +fun Schedule.Timestamp.updateDepartureForRealTime(departureDelay: Duration) = updateDepartureForRealTime(departure + departureDelay) + +fun Schedule.Timestamp.updateDepartureForRealTime(newDeparture: Instant) { + val departureDelay = newDeparture - departure + originalDepartureDelay = departureDelay + departureT = newDeparture.toMillis() + realTime = true +} + +fun Schedule.Timestamp.updateForRealTime(arrivalDelay: Duration?, departureDelay: Duration) { + updateDepartureForRealTime(departureDelay) + arrivalDelay?.let { updateArrivalForRealTime(it) } +} + +fun Schedule.Timestamp.updateForRealTime(newArrival: Instant?, newDeparture: Instant) { + updateDepartureForRealTime(newDeparture) + newArrival?.let { updateArrivalForRealTime(it) } +} + val Schedule.Timestamp.arrivalDiff get() = this.arrivalDiffMs?.milliseconds?.coerceAtLeast(Duration.ZERO) ?: Duration.ZERO -var Schedule.Timestamp.arrival: Instant - get() = arrivalT.millisToInstant() +val Schedule.Timestamp.arrival get() = arrivalT.millisToInstant() + +var Schedule.Timestamp.originalArrivalDelay: Duration + get() = originalArrivalDelayMs.milliseconds set(value) { - arrivalT = value.toMillis() + originalArrivalDelayMs = value.inWholeMilliseconds } + +val Schedule.Timestamp.originalArrival get() = arrival - originalArrivalDelay + +fun Schedule.Timestamp.updateArrivalForRealTime(arrivalDelay: Duration) = updateArrivalForRealTime(arrival + arrivalDelay) + +fun Schedule.Timestamp.updateArrivalForRealTime(newArrival: Instant) { + val arrivalDelay = newArrival - arrival + originalArrivalDelay = arrivalDelay + arrivalT = newArrival.toMillis() + realTime = true +} 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 ddd3055f..7702f9e9 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 @@ -7,6 +7,9 @@ import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure +import org.mtransit.android.commons.data.updateArrivalForRealTime +import org.mtransit.android.commons.data.updateDepartureForRealTime +import org.mtransit.android.commons.data.updateForRealTime import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optArrival import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optDelay @@ -172,10 +175,10 @@ internal fun applyDelaySTU( val stuDepartureDelay = gStopTimeUpdate.optDeparture .takeIf { gStopTimeUpdate.scheduleRelationship != GTUSTUScheduleRelationship.NO_DATA } .makeDelay(timestampOriginalDeparture, stuArrivalDelay, timestampOriginalArrivalDiff) - stuArrivalTime?.let { rdsTripTimestamp.arrival = it; rdsTripTimestamp.realTime = true } - ?: stuArrivalDelay?.let { rdsTripTimestamp.arrival += it; rdsTripTimestamp.realTime = true } - stuDepartureTime?.let { rdsTripTimestamp.departure = it; rdsTripTimestamp.realTime = true } - ?: stuDepartureDelay?.let { rdsTripTimestamp.departure += it; rdsTripTimestamp.realTime = true } + stuArrivalTime?.let { rdsTripTimestamp.updateArrivalForRealTime(newArrival = it) } + ?: stuArrivalDelay?.let { rdsTripTimestamp.updateArrivalForRealTime(arrivalDelay = it) } + stuDepartureTime?.let { rdsTripTimestamp.updateDepartureForRealTime(newDeparture = it) } + ?: stuDepartureDelay?.let { rdsTripTimestamp.updateDepartureForRealTime(departureDelay = it) } if (gStopTimeUpdate.scheduleRelationship == GTUSTUScheduleRelationship.SKIPPED) { rdsSchedule.removeTimestamp(rdsTripTimestamp) } @@ -208,19 +211,14 @@ internal fun applyDelay( ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.arrivalDiff if (currentDelay < Duration.ZERO) { - rdsTripTimestamp.arrival += currentDelay - rdsTripTimestamp.departure += currentDelay - rdsTripTimestamp.realTime = true + rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = currentDelay) return currentDelay // do not consume negative delay } else if (currentDiffBetweenArrivalAndDeparture <= currentDelay) { - rdsTripTimestamp.arrival += currentDelay val newDelay = (currentDelay - currentDiffBetweenArrivalAndDeparture).coerceAtLeast(Duration.ZERO) - rdsTripTimestamp.departure += newDelay - rdsTripTimestamp.realTime = true + rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = newDelay) return newDelay } else { - rdsTripTimestamp.arrival += currentDelay - rdsTripTimestamp.realTime = true + rdsTripTimestamp.updateArrivalForRealTime(currentDelay) return Duration.ZERO // all delay consumed } } diff --git a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt index 6a07f28e..eef46014 100644 --- a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt +++ b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt @@ -3,7 +3,10 @@ package org.mtransit.android.commons.data import org.mtransit.android.commons.secsToInstant import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds class ScheduleExtTests { @@ -12,14 +15,63 @@ class ScheduleExtTests { private const val DEPARTURE_MS = 1772722800L // 2026-03-06 10:00: } + @Test + fun test_departure_update() { + val departure = DEPARTURE_MS.secsToInstant() + val arrival = departure - 10.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + + timestamp.updateForRealTime(newArrival = arrival + 7.minutes, newDeparture = departure + 1.minutes) + + assertTrue { timestamp.isRealTime } + assertTrue { timestamp.isArrivalLate(minDelay = 1.minutes) } + assertEquals(arrival + 7.minutes, timestamp.arrival) + assertEquals(7.minutes, timestamp.originalArrivalDelay) + assertEquals(arrival, timestamp.originalArrival) + assertTrue { timestamp.isArrivalLate(minDelay = 1.minutes) } + assertEquals(departure + 1.minutes, timestamp.departure) + assertEquals(1.minutes, timestamp.originalDepartureDelay) + assertEquals(departure, timestamp.originalDeparture) + assertEquals(4.minutes, timestamp.arrivalDiff) + } + + @Test + fun test_departure_update_early() { + val departure = DEPARTURE_MS.secsToInstant() + val arrival = departure - 10.minutes + val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) + + timestamp.updateForRealTime(arrivalDelay = (-3).minutes, departureDelay = (-5).minutes) + + assertTrue { timestamp.isRealTime } + assertTrue { timestamp.isArrivalEarly() } + assertFalse { timestamp.isArrivalEarly(maxDelay = 5.minutes) } + assertEquals(arrival - 3.minutes, timestamp.arrival) + assertEquals((-3).minutes, timestamp.originalArrivalDelay) + assertEquals(arrival, timestamp.originalArrival) + assertTrue { timestamp.isDepartureEarly() } + assertFalse { timestamp.isDepartureEarly(maxDelay = 10.minutes) } + assertEquals(departure - 5.minutes, timestamp.departure) + assertEquals((-5).minutes, timestamp.originalDepartureDelay) + assertEquals(departure, timestamp.originalDeparture) + assertEquals(8.minutes, timestamp.arrivalDiff) + } + @Test fun test_departure_update_no_effect_on_arrival() { val departure = DEPARTURE_MS.secsToInstant() val arrival = departure - 1.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) - timestamp.departure += 1.minutes + timestamp.updateForRealTime(arrivalDelay = null, departureDelay = 1.minutes) + + assertTrue { timestamp.isRealTime } + assertFalse { timestamp.isArrivalLate() || timestamp.isArrivalEarly() } assertEquals(arrival, timestamp.arrival) + assertTrue { timestamp.isDepartureLate(minDelay = 30.seconds) } assertEquals(departure + 1.minutes, timestamp.departure) + assertEquals(1.minutes, timestamp.originalDepartureDelay) + assertEquals(departure, timestamp.originalDeparture) + assertEquals(2.minutes, timestamp.arrivalDiff) } } From 9cc99b666c403f5de241f6621566392557954646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 12 Mar 2026 11:09:31 -0400 Subject: [PATCH 35/39] Keep real-time GTFS-RT schedule if some schedule timestamp have trips with real-time info and departure in the past --- .../commons/data/ScheduleTimestamps.java | 26 ++++++++++--------- .../gtfs/GTFSScheduleTimestampsProvider.java | 2 +- .../ScheduleTimestampsProvider.java | 7 ++--- .../status/GTFSRealTimeTripUpdatesProvider.kt | 8 +++++- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java index b9d55846..8ca5380f 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleTimestamps.java @@ -54,11 +54,13 @@ public void sortTimestamps() { CollectionUtils.sort(this.timestamps, Schedule.TIMESTAMPS_COMPARATOR); } + @SuppressWarnings("unused") // main app only @NonNull public List getTimestamps() { return this.timestamps; } + @SuppressWarnings("unused") // main app only public int getTimestampsCount() { return this.timestamps.size(); } @@ -74,18 +76,18 @@ public String getSourceLabel() { @Nullable public static ScheduleTimestamps fromCursor(@NonNull Cursor cursor) { - String targetUUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_TARGET_UUID)); - long startsAtInMs = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_STARTS_AT)); - long endsAtInMs = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_ENDS_AT)); - ScheduleTimestamps scheduleTimestamps = new ScheduleTimestamps(targetUUID, startsAtInMs, endsAtInMs); - String extrasJSONString = cursor.getString(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_EXTRAS)); + final String targetUUID = cursor.getString(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_TARGET_UUID)); + final long startsAtInMs = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_STARTS_AT)); + final long endsAtInMs = cursor.getLong(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_ENDS_AT)); + final ScheduleTimestamps scheduleTimestamps = new ScheduleTimestamps(targetUUID, startsAtInMs, endsAtInMs); + final String extrasJSONString = cursor.getString(cursor.getColumnIndexOrThrow(ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_EXTRAS)); return fromExtraJSONString(scheduleTimestamps, extrasJSONString); } @Nullable private static ScheduleTimestamps fromExtraJSONString(ScheduleTimestamps scheduleTimestamps, String extrasJSONString) { try { - JSONObject json = extrasJSONString == null ? null : new JSONObject(extrasJSONString); + final JSONObject json = extrasJSONString == null ? null : new JSONObject(extrasJSONString); if (json == null) { return null; } @@ -102,9 +104,9 @@ private static ScheduleTimestamps fromExtraJSONString(ScheduleTimestamps schedul private static ScheduleTimestamps fromExtraJSON(ScheduleTimestamps scheduleTimestamps, JSONObject extrasJSON) { try { scheduleTimestamps.setSourceLabel(extrasJSON.optString(JSON_SOURCE_LABEL, null)); - JSONArray jTimestamps = extrasJSON.getJSONArray(JSON_TIMESTAMPS); + final JSONArray jTimestamps = extrasJSON.getJSONArray(JSON_TIMESTAMPS); for (int i = 0; i < jTimestamps.length(); i++) { - JSONObject jTimestamp = jTimestamps.getJSONObject(i); + final JSONObject jTimestamp = jTimestamps.getJSONObject(i); scheduleTimestamps.addTimestampWithoutSort(Schedule.Timestamp.parseJSON(jTimestamp)); } scheduleTimestamps.sortTimestamps(); @@ -117,7 +119,7 @@ private static ScheduleTimestamps fromExtraJSON(ScheduleTimestamps scheduleTimes @NonNull public Cursor toCursor() { - MatrixCursor cursor = new MatrixCursor(new String[]{ + final MatrixCursor cursor = new MatrixCursor(new String[]{ ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_TARGET_UUID, ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_STARTS_AT, ScheduleTimestampsProviderContract.Columns.T_SCHEDULE_TIMESTAMPS_K_ENDS_AT, @@ -130,7 +132,7 @@ public Cursor toCursor() { @Nullable private String getExtrasJSONString() { try { - JSONObject extrasJSON = getExtrasJSON(); + final JSONObject extrasJSON = getExtrasJSON(); return extrasJSON == null ? null : extrasJSON.toString(); } catch (Exception e) { MTLog.w(LOG_TAG, e, "Error while converting JSON to String!"); @@ -142,9 +144,9 @@ private String getExtrasJSONString() { @Nullable public JSONObject getExtrasJSON() { try { - JSONObject json = new JSONObject(); + final JSONObject json = new JSONObject(); json.put(JSON_SOURCE_LABEL, this.sourceLabel); - JSONArray jTimestamps = new JSONArray(); + final JSONArray jTimestamps = new JSONArray(); for (Schedule.Timestamp timestamp : this.timestamps) { jTimestamps.put(timestamp.toJSON()); } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java index 0657a8a7..efd549c0 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSScheduleTimestampsProvider.java @@ -117,7 +117,7 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro if (FeatureFlags.F_EXPORT_TRIP_ID_INTS) { allTimestamps = GTFSTripIdsUtils.updateTripIds(allTimestamps, provider); } - ScheduleTimestamps scheduleTimestamps = new ScheduleTimestamps(rds.getUUID(), startsAtInMs, endsAtInMs); + final ScheduleTimestamps scheduleTimestamps = new ScheduleTimestamps(rds.getUUID(), startsAtInMs, endsAtInMs); scheduleTimestamps.setSourceLabel(GTFSProvider.getSOURCE_LABEL(provider.requireContextCompat())); scheduleTimestamps.setTimestampsAndSort(allTimestamps); return scheduleTimestamps; diff --git a/src/main/java/org/mtransit/android/commons/provider/scheduletimestamp/ScheduleTimestampsProvider.java b/src/main/java/org/mtransit/android/commons/provider/scheduletimestamp/ScheduleTimestampsProvider.java index 36fed081..b1010aa4 100644 --- a/src/main/java/org/mtransit/android/commons/provider/scheduletimestamp/ScheduleTimestampsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/scheduletimestamp/ScheduleTimestampsProvider.java @@ -48,16 +48,17 @@ public static Cursor queryS(@NonNull ScheduleTimestampsProviderContract provider } private static Cursor getScheduleTimestamps(@NonNull ScheduleTimestampsProviderContract provider, @Nullable String selection) { - ScheduleTimestampsProviderContract.Filter scheduleTimestampsFilter = ScheduleTimestampsProviderContract.Filter.fromJSONString(selection); + final ScheduleTimestampsProviderContract.Filter scheduleTimestampsFilter = ScheduleTimestampsProviderContract.Filter.fromJSONString(selection); if (scheduleTimestampsFilter == null) { MTLog.w(LOG_TAG, "Error while parsing schedule timestamps filter '%s'!", selection); return getScheduleTimestampCursor(null); } - ScheduleTimestamps scheduleTimestamps = provider.getScheduleTimestamps(scheduleTimestampsFilter); + final ScheduleTimestamps scheduleTimestamps = provider.getScheduleTimestamps(scheduleTimestampsFilter); return getScheduleTimestampCursor(scheduleTimestamps); } - public static Cursor getScheduleTimestampCursor(ScheduleTimestamps scheduleTimestamps) { + @NonNull + public static Cursor getScheduleTimestampCursor(@Nullable ScheduleTimestamps scheduleTimestamps) { if (scheduleTimestamps == null) { return ContentProviderConstants.EMPTY_CURSOR; } 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 6f1088b9..c6b84b02 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 @@ -150,9 +150,15 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { uuidSchedule ?: return null sortedRDS ?: return null processRDTripUpdates(rdTripUpdates, uuidSchedule, sortedRDS) + val tripsWithRealTime = uuidSchedule.values + .asSequence() + .mapNotNull { it?.timestamps }.flatten() + .filter { it.isRealTime } + .map { it.tripId } + .toSet() // distinct uuidSchedule.values.filterNotNull().forEach { schedule -> - if (!schedule.timestamps.any { it.isRealTime }) return@forEach val now = TimeUtilsK.currentInstant() + if (!schedule.timestamps.any { it.isRealTime || (it.tripId in tripsWithRealTime && it.departure < now) }) return@forEach var oldestDateForRealTime = now - 1.minutes var maxFutureDateForRealTime = now + 12.hours val (past, future) = schedule.timestamps.partition { it.departure < now } From 550b8575cc8e0459d8e93316457e3a42d306327d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 12 Mar 2026 14:08:00 -0400 Subject: [PATCH 36/39] Store no-data when no-real-time to void re-compute + synchronized lock to make cache only once --- .../android/commons/data/ScheduleExt.kt | 12 +++++++++++ .../status/GTFSRealTimeTripUpdatesProvider.kt | 20 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 1920817e..c95266e3 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -7,6 +7,18 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant +fun Schedule.toNoData() = Schedule( + id, + targetUUID, + lastUpdateInMs, + maxValidityInMs, + readFromSourceAtInMs, + providerPrecisionInMs, + isNoPickup, + sourceLabel, + true // NO DATA +) + fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null, stopSequence: Int? = null) = Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { arrival?.let { this.arrivalT = it.toMillis() } 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 c6b84b02..50c06a4f 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 @@ -12,6 +12,7 @@ import org.mtransit.android.commons.data.RouteDirectionStop import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.departure +import org.mtransit.android.commons.data.toNoData import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.isIGNORE_DIRECTION import org.mtransit.android.commons.provider.gtfs.GtfsRealTimeStorage @@ -89,7 +90,19 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { }?.takeIf { tripIds -> tripIds.isNotEmpty() } // trip IDs REQUIRED for GTFS Trip Updates ?: return null return getCachedStatusS(filter.targetUUID, tripIds) - ?: makeCachedStatusFromAgencyData(filter, tripIds) + ?: makeCachedStatusFromAgencyDataLock(filter, tripIds) + } + + private val tripUpdateLock = Any() + + private fun GTFSRealTimeProvider.makeCachedStatusFromAgencyDataLock( + filter: Schedule.ScheduleStatusFilter, + tripIds: List + ): POIStatus? { + synchronized(tripUpdateLock) { + return getCachedStatusS(filter.targetUUID, tripIds) // try another time + ?: makeCachedStatusFromAgencyData(filter, tripIds) + } } val GTFSRealTimeProvider.ignoreDirection get() = isIGNORE_DIRECTION(this.requireContextCompat()) @@ -158,7 +171,10 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { .toSet() // distinct uuidSchedule.values.filterNotNull().forEach { schedule -> val now = TimeUtilsK.currentInstant() - if (!schedule.timestamps.any { it.isRealTime || (it.tripId in tripsWithRealTime && it.departure < now) }) return@forEach + if (!schedule.timestamps.any { it.isRealTime || (it.tripId in tripsWithRealTime && it.departure < now) }) { + cacheStatus(schedule.toNoData()) // avoid re-run + return@forEach + } var oldestDateForRealTime = now - 1.minutes var maxFutureDateForRealTime = now + 12.hours val (past, future) = schedule.timestamps.partition { it.departure < now } From fc4fd40f413e8fdbc60a95ede0a7ee41b6356542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 12 Mar 2026 15:15:43 -0400 Subject: [PATCH 37/39] wip --- .../android/commons/data/ScheduleExt.kt | 32 +++++++++++++++++-- .../GTFSRealTimeTripUpdatesProviderExt.kt | 8 ++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index c95266e3..e86f7647 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -1,5 +1,7 @@ package org.mtransit.android.commons.data +import org.mtransit.android.commons.Constants +import org.mtransit.android.commons.MTLog.formatDateTime import org.mtransit.android.commons.millisToInstant import org.mtransit.android.commons.toMillis import kotlin.time.Duration @@ -51,7 +53,7 @@ val Schedule.Timestamp.originalDeparture get() = departure - originalDepartureDe fun Schedule.Timestamp.updateDepartureForRealTime(departureDelay: Duration) = updateDepartureForRealTime(departure + departureDelay) fun Schedule.Timestamp.updateDepartureForRealTime(newDeparture: Instant) { - val departureDelay = newDeparture - departure + val departureDelay = newDeparture - originalDeparture originalDepartureDelay = departureDelay departureT = newDeparture.toMillis() realTime = true @@ -82,8 +84,34 @@ val Schedule.Timestamp.originalArrival get() = arrival - originalArrivalDelay fun Schedule.Timestamp.updateArrivalForRealTime(arrivalDelay: Duration) = updateArrivalForRealTime(arrival + arrivalDelay) fun Schedule.Timestamp.updateArrivalForRealTime(newArrival: Instant) { - val arrivalDelay = newArrival - arrival + val arrivalDelay = newArrival - originalArrival originalArrivalDelay = arrivalDelay arrivalT = newArrival.toMillis() realTime = true } + +val Schedule.hasRealTime get() = this.timestamps.any { it.isRealTime } + +@Suppress("unsued") +fun Schedule.Timestamp.toStringShort() = buildString { + append("T{") + arrivalTIfDifferent?.let { + append("a=").append(if (Constants.DEBUG) formatDateTime(arrivalT) else arrivalT) + if (originalArrivalDelayMs != 0L) { + append("[+/-:").append(originalArrivalDelayMs).append("]") + } + append(",") + } + append("d=").append(if (Constants.DEBUG) formatDateTime(departureT) else departureT) + if (originalDepartureDelayMs != 0L) { + append("[+/-:").append(originalDepartureDelayMs).append("]") + } + // append(",") + if (isRealTime) { + append("[RT]") + } + if (isOldSchedule) { + append("[OLD]") + } + append("}") +} 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 7702f9e9..50a48458 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 @@ -136,8 +136,8 @@ internal fun processRDTripUpdate( } } -private fun Schedule.findClosestTripTimestamp(tripId: String, stopSequence: Int) = - timestamps.filter { it.tripId == tripId } +fun Iterable.findClosestTripTimestamp(tripId: String, stopSequence: Int) = + this.filter { it.tripId == tripId } .filter { timestamp -> (timestamp.stopSequenceOrNull == null || timestamp.stopSequenceOrNull == stopSequence) }.let { rdsTripTimestamps -> @@ -156,7 +156,7 @@ internal fun applyDelaySTU( gStopTimeUpdate: GTUStopTimeUpdate, currentDelay: Duration? = null, ): Duration? { - val rdsTripTimestamp = rdsSchedule?.findClosestTripTimestamp(tripId, stopSequence) + val rdsTripTimestamp = rdsSchedule?.timestamps?.findClosestTripTimestamp(tripId, stopSequence) ?: return null // impossible to handle val timestampOriginalArrival = rdsTripTimestamp.arrival val timestampOriginalDeparture = rdsTripTimestamp.departure @@ -207,7 +207,7 @@ internal fun applyDelay( currentDelay: Duration? ): Duration? { currentDelay ?: return null - val rdsTripTimestamp = rdsSchedule?.findClosestTripTimestamp(tripId, stopSequence) + val rdsTripTimestamp = rdsSchedule?.timestamps?.findClosestTripTimestamp(tripId, stopSequence) ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.arrivalDiff if (currentDelay < Duration.ZERO) { From fe2de4d8e1435bc024f7a2ae64c15d801caeec71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 13 Mar 2026 08:52:54 -0400 Subject: [PATCH 38/39] fix time precision --- .../mtransit/android/commons/TimeUtilsK.kt | 20 ++++ .../android/commons/data/ScheduleExt.kt | 36 ++++-- .../provider/gtfs/GTFSStatusProvider.java | 2 +- .../status/GTFSRealTimeTripUpdatesProvider.kt | 3 +- .../GTFSRealTimeTripUpdatesProviderExt.kt | 12 +- .../android/commons/data/ScheduleExtTests.kt | 104 +++++++++++++++++- .../GTFSRealTimeTripUpdatesProviderTests.kt | 5 +- 7 files changed, 164 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt b/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt index b6814f64..bfe75b92 100644 --- a/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt +++ b/src/main/java/org/mtransit/android/commons/TimeUtilsK.kt @@ -1,6 +1,7 @@ package org.mtransit.android.commons import kotlin.math.abs +import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Instant @@ -26,8 +27,27 @@ object TimeUtilsK { fun Long.millisToInstant() = Instant.fromEpochMilliseconds(this) fun Long.secsToInstant() = Instant.fromEpochSeconds(this) + +@Suppress("unused") fun Int.secsToInstant() = this.toLong().secsToInstant() fun Instant.toMillis() = this.toEpochMilliseconds() fun Instant.toSecs() = this.epochSeconds + +fun Instant.roundToNearest(interval: Duration): Instant { + val intervalMillis = interval.inWholeMilliseconds + .takeUnless { it == 0L } ?: return this + return ((this.toMillis() + (intervalMillis / 2L)) / intervalMillis * intervalMillis).millisToInstant() +} + +fun Instant.floorBy(period: Duration, down: Boolean = true) = + (this % period).let { rem -> + when { + rem == Duration.ZERO -> this + down -> this - rem + else -> this + (period - rem) + } + } + +operator fun Instant.rem(period: Duration): Duration = (this.toMillis() % period.inWholeMilliseconds).milliseconds diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index e86f7647..686310a1 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -2,7 +2,9 @@ package org.mtransit.android.commons.data import org.mtransit.android.commons.Constants import org.mtransit.android.commons.MTLog.formatDateTime +import org.mtransit.android.commons.floorBy import org.mtransit.android.commons.millisToInstant +import org.mtransit.android.commons.roundToNearest import org.mtransit.android.commons.toMillis import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -21,6 +23,8 @@ fun Schedule.toNoData() = Schedule( true // NO DATA ) +val Schedule.providerPrecision get() = providerPrecisionInMs.milliseconds + fun Instant.toScheduleTimestamp(localTimeZoneId: String, arrival: Instant? = null, tripId: String? = null, stopSequence: Int? = null) = Schedule.Timestamp(this.toMillis(), localTimeZoneId).apply { arrival?.let { this.arrivalT = it.toMillis() } @@ -50,7 +54,16 @@ var Schedule.Timestamp.originalDepartureDelay: Duration val Schedule.Timestamp.originalDeparture get() = departure - originalDepartureDelay -fun Schedule.Timestamp.updateDepartureForRealTime(departureDelay: Duration) = updateDepartureForRealTime(departure + departureDelay) +fun Schedule.Timestamp.updateDepartureForRealTime(departureDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { + val maxPrecision = currentPrecision.coerceAtLeast(delayPrecision) + val newDeparture = departure + departureDelay + val roundedDeparture = if (departureDelay.absoluteValue > maxPrecision.div(2)) { + newDeparture.roundToNearest(maxPrecision) + } else { + newDeparture.floorBy(maxPrecision, down = departureDelay.isPositive()) + } + updateDepartureForRealTime(roundedDeparture) +} fun Schedule.Timestamp.updateDepartureForRealTime(newDeparture: Instant) { val departureDelay = newDeparture - originalDeparture @@ -59,9 +72,9 @@ fun Schedule.Timestamp.updateDepartureForRealTime(newDeparture: Instant) { realTime = true } -fun Schedule.Timestamp.updateForRealTime(arrivalDelay: Duration?, departureDelay: Duration) { - updateDepartureForRealTime(departureDelay) - arrivalDelay?.let { updateArrivalForRealTime(it) } +fun Schedule.Timestamp.updateForRealTime(arrivalDelay: Duration?, departureDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { + updateDepartureForRealTime(departureDelay, currentPrecision, delayPrecision) + arrivalDelay?.let { updateArrivalForRealTime(it, currentPrecision, delayPrecision) } } fun Schedule.Timestamp.updateForRealTime(newArrival: Instant?, newDeparture: Instant) { @@ -81,7 +94,16 @@ var Schedule.Timestamp.originalArrivalDelay: Duration val Schedule.Timestamp.originalArrival get() = arrival - originalArrivalDelay -fun Schedule.Timestamp.updateArrivalForRealTime(arrivalDelay: Duration) = updateArrivalForRealTime(arrival + arrivalDelay) +fun Schedule.Timestamp.updateArrivalForRealTime(arrivalDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { + val maxPrecision = currentPrecision.coerceAtLeast(delayPrecision) + val newArrival = arrival + arrivalDelay + val roundedArrival = if (arrivalDelay.absoluteValue > maxPrecision.div(2)) { + newArrival.roundToNearest(maxPrecision) + } else { + newArrival.floorBy(maxPrecision, down = arrivalDelay.isPositive()) + } + updateArrivalForRealTime(roundedArrival) +} fun Schedule.Timestamp.updateArrivalForRealTime(newArrival: Instant) { val arrivalDelay = newArrival - originalArrival @@ -90,9 +112,10 @@ fun Schedule.Timestamp.updateArrivalForRealTime(newArrival: Instant) { realTime = true } +@Suppress("unused") val Schedule.hasRealTime get() = this.timestamps.any { it.isRealTime } -@Suppress("unsued") +@Suppress("unused") fun Schedule.Timestamp.toStringShort() = buildString { append("T{") arrivalTIfDifferent?.let { @@ -106,7 +129,6 @@ fun Schedule.Timestamp.toStringShort() = buildString { if (originalDepartureDelayMs != 0L) { append("[+/-:").append(originalDepartureDelayMs).append("]") } - // append(",") if (isRealTime) { append("[RT]") } diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java index 4b144642..5bc7cdc9 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSStatusProvider.java @@ -137,7 +137,7 @@ public static long getMinDurationBetweenRefreshInMs(boolean inFocus) { return STATUS_MIN_DURATION_BETWEEN_REFRESH_IN_MS; } - private static final long PROVIDER_PRECISION_IN_MS = TimeUnit.MINUTES.toMillis(1L); + public static final long PROVIDER_PRECISION_IN_MS = TimeUnit.MINUTES.toMillis(1L); private static final long PROVIDER_READ_FROM_SOURCE_AT_IN_MS = 0; // it doesn't get older than that 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 50c06a4f..248be73f 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 @@ -45,7 +45,8 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { override fun getLogTag() = LOG_TAG - val PROVIDER_PRECISION_IN_MS = 10.seconds.inWholeMilliseconds + val PROVIDER_PRECISION = 10.seconds + val PROVIDER_PRECISION_IN_MS = PROVIDER_PRECISION.inWholeMilliseconds val TRIP_UPDATE_MAX_VALIDITY_IN_MS = 1.hours.inWholeMilliseconds 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 50a48458..4ca7ea88 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 @@ -7,6 +7,7 @@ import org.mtransit.android.commons.data.Schedule import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure +import org.mtransit.android.commons.data.providerPrecision import org.mtransit.android.commons.data.updateArrivalForRealTime import org.mtransit.android.commons.data.updateDepartureForRealTime import org.mtransit.android.commons.data.updateForRealTime @@ -25,6 +26,7 @@ import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.toStringExt import org.mtransit.android.commons.provider.gtfs.parseStopId import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.status.GTFSRealTimeTripUpdatesProvider.LOG_TAG +import org.mtransit.android.commons.provider.status.GTFSRealTimeTripUpdatesProvider.PROVIDER_PRECISION import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant @@ -176,9 +178,9 @@ 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) } + ?: stuArrivalDelay?.let { rdsTripTimestamp.updateArrivalForRealTime(arrivalDelay = it, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) } stuDepartureTime?.let { rdsTripTimestamp.updateDepartureForRealTime(newDeparture = it) } - ?: stuDepartureDelay?.let { rdsTripTimestamp.updateDepartureForRealTime(departureDelay = it) } + ?: stuDepartureDelay?.let { rdsTripTimestamp.updateDepartureForRealTime(departureDelay = it, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) } if (gStopTimeUpdate.scheduleRelationship == GTUSTUScheduleRelationship.SKIPPED) { rdsSchedule.removeTimestamp(rdsTripTimestamp) } @@ -211,14 +213,14 @@ internal fun applyDelay( ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.arrivalDiff if (currentDelay < Duration.ZERO) { - rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = currentDelay) + rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = currentDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = 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) + rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = newDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) return newDelay } else { - rdsTripTimestamp.updateArrivalForRealTime(currentDelay) + rdsTripTimestamp.updateArrivalForRealTime(currentDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) return Duration.ZERO // all delay consumed } } diff --git a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt index eef46014..d11299ac 100644 --- a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt +++ b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt @@ -41,7 +41,7 @@ class ScheduleExtTests { val arrival = departure - 10.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) - timestamp.updateForRealTime(arrivalDelay = (-3).minutes, departureDelay = (-5).minutes) + timestamp.updateForRealTime(arrivalDelay = (-3).minutes, departureDelay = (-5).minutes, currentPrecision = 1.minutes, delayPrecision = 10.seconds) assertTrue { timestamp.isRealTime } assertTrue { timestamp.isArrivalEarly() } @@ -63,7 +63,7 @@ class ScheduleExtTests { val arrival = departure - 1.minutes val timestamp = departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival) - timestamp.updateForRealTime(arrivalDelay = null, departureDelay = 1.minutes) + timestamp.updateForRealTime(arrivalDelay = null, departureDelay = 1.minutes, currentPrecision = 1.minutes, delayPrecision = 10.seconds) assertTrue { timestamp.isRealTime } assertFalse { timestamp.isArrivalLate() || timestamp.isArrivalEarly() } @@ -74,4 +74,104 @@ class ScheduleExtTests { assertEquals(departure, timestamp.originalDeparture) assertEquals(2.minutes, timestamp.arrivalDiff) } + + @Test + fun test_updateArrivalForRealTime() { + val departure = DEPARTURE_MS.secsToInstant() + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = (-61).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure - 1.minutes, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = (-59).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure - 1.minutes, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = (-30).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = (-15).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = 15.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = 59.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 1.minutes, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = 61.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 1.minutes, result.arrival) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateArrivalForRealTime(arrivalDelay = 1.minutes + 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 2.minutes, result.arrival) + } + } + + @Test + fun test_updateDepartureForRealTime() { + val departure = DEPARTURE_MS.secsToInstant() + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = (-61).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure - 1.minutes, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = (-59).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure - 1.minutes, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = (-30).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = (-15).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = 15.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = 59.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 1.minutes, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = 61.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 1.minutes, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = 1.minutes + 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 2.minutes, result.departure) + } + } } 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 7b59442b..3dc62331 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 @@ -14,6 +14,7 @@ import org.mtransit.android.commons.data.arrival import org.mtransit.android.commons.data.arrivalDiff import org.mtransit.android.commons.data.departure import org.mtransit.android.commons.data.toScheduleTimestamp +import org.mtransit.android.commons.provider.gtfs.GTFSStatusProvider import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.delayDuration import org.mtransit.android.commons.secsToInstant import org.mtransit.android.commons.toSecs @@ -28,8 +29,8 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship -import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent as GTUStopTimeEvent +import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTUStopTimeUpdate import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship as GTUSTUScheduleRelationship class GTFSRealTimeTripUpdatesProviderTests { @@ -846,7 +847,7 @@ class GTFSRealTimeTripUpdatesProviderTests { nowInMs, nowInMs, nowInMs, - 10.seconds.inWholeMilliseconds, + GTFSStatusProvider.PROVIDER_PRECISION_IN_MS, false, null, false From fa8d4ab35b517c05b1989a3a64575ef8dabebac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 13 Mar 2026 09:47:09 -0400 Subject: [PATCH 39/39] wip --- .../android/commons/data/ScheduleExt.kt | 35 +++++---- .../GTFSRealTimeTripUpdatesProviderExt.kt | 2 +- .../android/commons/data/ScheduleExtTests.kt | 77 ++++++++++++------- 3 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt index 686310a1..895cc8af 100644 --- a/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt +++ b/src/main/java/org/mtransit/android/commons/data/ScheduleExt.kt @@ -54,15 +54,12 @@ var Schedule.Timestamp.originalDepartureDelay: Duration val Schedule.Timestamp.originalDeparture get() = departure - originalDepartureDelay +/** + * It's better to be early at the stop, than late and miss the vehicle departure -> truncate (floor by) to early w/ precision + */ fun Schedule.Timestamp.updateDepartureForRealTime(departureDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { val maxPrecision = currentPrecision.coerceAtLeast(delayPrecision) - val newDeparture = departure + departureDelay - val roundedDeparture = if (departureDelay.absoluteValue > maxPrecision.div(2)) { - newDeparture.roundToNearest(maxPrecision) - } else { - newDeparture.floorBy(maxPrecision, down = departureDelay.isPositive()) - } - updateDepartureForRealTime(roundedDeparture) + updateDepartureForRealTime(computeInstant(departure, departureDelay, maxPrecision)) } fun Schedule.Timestamp.updateDepartureForRealTime(newDeparture: Instant) { @@ -72,6 +69,9 @@ fun Schedule.Timestamp.updateDepartureForRealTime(newDeparture: Instant) { realTime = true } +fun Schedule.Timestamp.updateForRealTime(delay: Duration, currentPrecision: Duration, delayPrecision: Duration) = + updateForRealTime(arrivalDelay = delay, departureDelay = delay, currentPrecision = currentPrecision, delayPrecision = delayPrecision) + fun Schedule.Timestamp.updateForRealTime(arrivalDelay: Duration?, departureDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { updateDepartureForRealTime(departureDelay, currentPrecision, delayPrecision) arrivalDelay?.let { updateArrivalForRealTime(it, currentPrecision, delayPrecision) } @@ -94,15 +94,22 @@ var Schedule.Timestamp.originalArrivalDelay: Duration val Schedule.Timestamp.originalArrival get() = arrival - originalArrivalDelay -fun Schedule.Timestamp.updateArrivalForRealTime(arrivalDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { - val maxPrecision = currentPrecision.coerceAtLeast(delayPrecision) - val newArrival = arrival + arrivalDelay - val roundedArrival = if (arrivalDelay.absoluteValue > maxPrecision.div(2)) { - newArrival.roundToNearest(maxPrecision) +private fun computeInstant(initialInstant: Instant, delay: Duration, precision: Duration, canRoundToNearest: Boolean = false, canRoundUp: Boolean = false): Instant { + val newInstant = initialInstant + delay + val roundedNewInstant = if (canRoundToNearest && delay.absoluteValue > precision.div(2)) { + newInstant.roundToNearest(precision) } else { - newArrival.floorBy(maxPrecision, down = arrivalDelay.isPositive()) + newInstant.floorBy(precision, down = !canRoundUp || delay.isPositive()) } - updateArrivalForRealTime(roundedArrival) + return roundedNewInstant +} + +/** + * Arrival is almost never shown in UI, has to be before departure -> same rule as departure + */ +fun Schedule.Timestamp.updateArrivalForRealTime(arrivalDelay: Duration, currentPrecision: Duration, delayPrecision: Duration) { + val maxPrecision = currentPrecision.coerceAtLeast(delayPrecision) + updateArrivalForRealTime(computeInstant(arrival, arrivalDelay, maxPrecision)) } fun Schedule.Timestamp.updateArrivalForRealTime(newArrival: Instant) { 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 4ca7ea88..1698de88 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 @@ -213,7 +213,7 @@ internal fun applyDelay( ?: return currentDelay val currentDiffBetweenArrivalAndDeparture = rdsTripTimestamp.arrivalDiff if (currentDelay < Duration.ZERO) { - rdsTripTimestamp.updateForRealTime(arrivalDelay = currentDelay, departureDelay = currentDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) + rdsTripTimestamp.updateForRealTime(delay = currentDelay, currentPrecision = rdsSchedule.providerPrecision, delayPrecision = PROVIDER_PRECISION) return currentDelay // do not consume negative delay } else if (currentDiffBetweenArrivalAndDeparture <= currentDelay) { val newDelay = (currentDelay - currentDiffBetweenArrivalAndDeparture).coerceAtLeast(Duration.ZERO) diff --git a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt index d11299ac..eb4fd4a1 100644 --- a/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt +++ b/src/test/java/org/mtransit/android/commons/data/ScheduleExtTests.kt @@ -76,52 +76,70 @@ class ScheduleExtTests { } @Test - fun test_updateArrivalForRealTime() { + fun test_updateForRealTime_w_arrival() { val departure = DEPARTURE_MS.secsToInstant() - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = (-61).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = (-61).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure - 2.minutes, result.arrival) + assertEquals(departure - 2.minutes, result.departure) + assertTrue { result.arrival <= result.departure } + } + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = (-59).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure - 1.minutes, result.arrival) + assertEquals(departure - 1.minutes, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = (-59).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = (-30).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure - 1.minutes, result.arrival) + assertEquals(departure - 1.minutes, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = (-30).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = (-15).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> - assertEquals(departure, result.arrival) + assertTrue { result.arrival <= result.departure } + assertEquals(departure - 1.minutes, result.arrival) + assertEquals(departure - 1.minutes, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = (-15).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = 15.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure, result.arrival) + assertEquals(departure, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = 15.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure, result.arrival) + assertEquals(departure, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = 59.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure, result.arrival) + assertEquals(departure, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = 59.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = 61.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure + 1.minutes, result.arrival) + assertEquals(departure + 1.minutes, result.departure) } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = 61.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + departure.toScheduleTimestamp(LOCAL_TZ_ID, arrival = departure).apply { + updateForRealTime(delay = 1.minutes + 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> + assertTrue { result.arrival <= result.departure } assertEquals(departure + 1.minutes, result.arrival) - } - departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { - updateArrivalForRealTime(arrivalDelay = 1.minutes + 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) - }.let { result -> - assertEquals(departure + 2.minutes, result.arrival) + assertEquals(departure + 1.minutes, result.departure) } } @@ -131,7 +149,7 @@ class ScheduleExtTests { departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = (-61).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> - assertEquals(departure - 1.minutes, result.departure) + assertEquals(departure - 2.minutes, result.departure) } departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = (-59).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) @@ -141,12 +159,12 @@ class ScheduleExtTests { departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = (-30).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> - assertEquals(departure, result.departure) + assertEquals(departure - 1.minutes, result.departure) } departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = (-15).seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> - assertEquals(departure, result.departure) + assertEquals(departure - 1.minutes, result.departure) } departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = 15.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) @@ -161,7 +179,7 @@ class ScheduleExtTests { departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = 59.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> - assertEquals(departure + 1.minutes, result.departure) + assertEquals(departure, result.departure) } departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = 61.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) @@ -170,6 +188,11 @@ class ScheduleExtTests { } departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { updateDepartureForRealTime(departureDelay = 1.minutes + 30.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) + }.let { result -> + assertEquals(departure + 1.minutes, result.departure) + } + departure.toScheduleTimestamp(LOCAL_TZ_ID).apply { + updateDepartureForRealTime(departureDelay = 2.minutes + 36.seconds, currentPrecision = 1.minutes, delayPrecision = 10.seconds) }.let { result -> assertEquals(departure + 2.minutes, result.departure) }