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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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,