From f6516379e741c184a3a2f9ef91c8a0fcc52dcce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 30 Jul 2026 10:56:02 -0400 Subject: [PATCH 1/9] GTFS-RT > ignore trip updates too much into the future... - #190 --- .../GTFSRealTimeServiceAlertsProvider.kt | 3 +- .../status/GTFSRealTimeTripUpdatesProvider.kt | 106 ++++++++++-------- .../GTFSRealTimeVehiclePositionsProvider.kt | 3 +- .../GTFSRealTimeTripUpdatesProviderTests.kt | 9 -- 4 files changed, 66 insertions(+), 55 deletions(-) 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 0ebe931d..9d39b9bf 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 @@ -109,8 +109,9 @@ object GTFSRealTimeServiceAlertsProvider : MTLog.Loggable { // Best practices: 10 minutes // https://gtfs.org/documentation/realtime/realtime-best-practices/#feed-publishing-general-practices + // Montréal-based competitor: no limit @JvmStatic - val SERVICE_ALERTS_MAX_AGE_MS = 10.times(3).minutes.inWholeMilliseconds + val SERVICE_ALERTS_MAX_AGE_MS = 10.times(13).minutes.inWholeMilliseconds @JvmStatic fun GTFSRealTimeProvider.getNew(filter: ServiceUpdateProviderContract.Filter): ServiceUpdates? { 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 765529d2..c32d19eb 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 @@ -18,10 +18,13 @@ import org.mtransit.android.commons.data.makeSchedule import org.mtransit.android.commons.data.toNoData import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.ALLOW_IGNORE_TRIP_DESCRIPTOR_DIRECTION_ID +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.optDirectionIdValid import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestampMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripIdNotEmpty @@ -110,24 +113,39 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { private fun GTripUpdate.isUseful(nowMs: Long): Boolean { optTimestampMs?.let { tuTimestamp -> if (tuTimestamp + TRIP_UPDATE_MAX_AGE_MS < nowMs) { - MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(nowMs - tuTimestamp).toDurationLog()} old: ${this.toStringExt()}") - return false + MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(nowMs - tuTimestamp).toDurationLog()} old: ${this.optTrip?.toStringExt(true)}") + return false // not useful (too old to display) } } + optStopTimeUpdateList?.firstOrNull()?.let { firstStu -> + if (firstStu.optDeparture?.hasDelay() == true || firstStu.optArrival?.hasDelay() == true) return@let + val timeMs = firstStu.optDeparture?.optTimeMs ?: firstStu.optArrival?.optTimeMs ?: return@let + if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { + MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(timeMs - nowMs).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") + return false // not useful (too far in advance to display) + } + } + if (optDelay != null + || optStopTimeUpdateList?.isNotEmpty() == true + ) { + return true // useful + } optTrip?.let { td -> // cannot match w/ static data - if (optDelay != null - || optStopTimeUpdateList?.isNotEmpty() == true - || td.optScheduleRelationship?.let { it != GTDScheduleRelationship.SCHEDULED } == true - ) { + if (td.optScheduleRelationship?.let { it != GTDScheduleRelationship.SCHEDULED } == true) { return true // useful } } + MTLog.w(LOG_TAG, "isUseful() > IGNORE (why?): ${this.toStringExt()}") return false // not useful } // Best practices: 90 seconds // https://gtfs.org/documentation/realtime/realtime-best-practices/#feed-publishing-general-practices - private val TRIP_UPDATE_MAX_AGE_MS = 90.times(3).seconds.inWholeMilliseconds + // Montréal-based competitor: 10 minutes + private val TRIP_UPDATE_MAX_AGE_MS = 90.times(7).seconds.inWholeMilliseconds + + // Montréal-based competitor: 4.5 hours (?) + private val FUTURE_TRIP_UPDATE_MAX_DIFF_MS = 90.minutes.times(3).inWholeMilliseconds private fun GTFSRealTimeProvider.makeCachedStatusFromAgencyData( context: Context, @@ -155,7 +173,6 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { val targetAuthority = filter.targetAuthority val targetRouteIdHash = targetRoute.originalIdHash.toString() val targetDirectionOriginalId = targetDirection.originalDirectionIdOrNull - var tripIdsOutOfSync = false if (DEBUG_STATIC_RT_MATCH) { MTLog.d(LOG_TAG, "makeCachedStatusFromAgencyData() > target trip IDs [${staticRDTripIds.size}]:") staticRDTripIds.chunked(9).forEach { @@ -169,7 +186,6 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { context.getRDSSchedule(targetAuthority, sortedRDS, filter.isIncludeCancelledTimestampsOrDefault) } val rdTripUpdates = gTripUpdates - .filter { it.isUseful(nowMs) } .filter { gTripUpdate -> gTripUpdate.optTrip?.match( targetRouteIdHash = targetRouteIdHash, @@ -178,20 +194,12 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { ignoreDirection = ignoreDirection, parseRouteId = ::parseRouteId, parseTripId = ::parseTripId, - setTripIdsOutOfSync = { - tripIdsOutOfSync = it - }, ) == true } + .filter { it.isUseful(nowMs) } .filterDuplicatesTrips() .takeIf { it.isNotEmpty() } ?.mapNotNull { gTripUpdate -> gTripUpdate.optTrip?.let { it to gTripUpdate } } - if (tripIdsOutOfSync) { - MTLog.i( - LOG_TAG, - "Trip IDs (might be) out of sync (or real-time route/direction ID provided) for route '${targetRoute.shortestName}' direction '${targetDirection.headsignValue}'." - ) - } rdTripUpdates ?: run { sortedRDS.forEach { rds -> rds.makeSchedule( @@ -247,6 +255,9 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { } } + // Montréal-based competitor: match with 'trip_id' first, + // else use 'route_id', 'direction_id', 'start_date' & 'start_time' to try to match correct trip as fallback + @VisibleForTesting internal fun GTripDescriptor.match( targetRouteIdHash: String, @@ -255,42 +266,49 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { ignoreDirection: Boolean, parseRouteId: (GTripDescriptor) -> String?, parseTripId: (GTripDescriptor) -> String?, - setTripIdsOutOfSync: (Boolean) -> Unit, ): Boolean { - parseRouteId(this)?.let { rtRouteIdHash -> - if (rtRouteIdHash != targetRouteIdHash) { - // if (DEBUG_STATIC_RT_MATCH) { // too much log - // MTLog.d(LOG_TAG, "match() > IGNORE: wrong route ID '$rtRouteIdHash' (t:$targetRouteIdHash)") - // } - return false // NOT A MATCH + parseTripId(this)?.let { tripId -> + if (tripId in staticRDTripIds) { + return true // MATCH } } - @Suppress("SimplifyBooleanWithConstants") - if (!ALLOW_IGNORE_TRIP_DESCRIPTOR_DIRECTION_ID || optTripIdNotEmpty == null) { - optDirectionIdValid?.takeIf { !ignoreDirection }?.let { directionId -> - if (directionId != targetDirectionOriginalId) { - if (DEBUG_STATIC_RT_MATCH) { - MTLog.d( - LOG_TAG, - "match() > IGNORE: wrong direction ID '$directionId' for ${toStringExt(short = true)}" - ) - } + @Suppress("ConstantConditionIf") + if (false) { // FIXME later try to match with route_id & direction_id & start date+time + parseRouteId(this)?.let { rtRouteIdHash -> + if (rtRouteIdHash != targetRouteIdHash) { + // if (DEBUG_STATIC_RT_MATCH) { // too much log + // MTLog.d(LOG_TAG, "match() > IGNORE: wrong route ID '$rtRouteIdHash' (t:$targetRouteIdHash)") + // } return false // NOT A MATCH } } - } - parseTripId(this)?.let { tripId -> - if (tripId !in staticRDTripIds) { - if (hasRouteId()) { - if (DEBUG_STATIC_RT_MATCH) { - MTLog.d(LOG_TAG, "match() > IGNORE: wrong trip ID ($tripId) for ${toStringExt(short = true)}") + @Suppress("SimplifyBooleanWithConstants") + if (!ALLOW_IGNORE_TRIP_DESCRIPTOR_DIRECTION_ID || optTripIdNotEmpty == null) { + optDirectionIdValid?.takeIf { !ignoreDirection }?.let { directionId -> + if (directionId != targetDirectionOriginalId) { + if (DEBUG_STATIC_RT_MATCH) { + MTLog.d( + LOG_TAG, + "match() > IGNORE: wrong direction ID '$directionId' for ${toStringExt(short = true)}" + ) + } + return false // NOT A MATCH } - setTripIdsOutOfSync(true) } - return false // NOT A MATCH } + parseTripId(this)?.let { tripId -> + if (tripId !in staticRDTripIds) { + if (hasRouteId()) { + if (DEBUG_STATIC_RT_MATCH) { + MTLog.d(LOG_TAG, "match() > IGNORE: wrong trip ID ($tripId) for ${toStringExt(short = true)}") + } + } + return false // NOT A MATCH + } + } + return true // MATCH } - return true // MATCH + return false // NOT A MATCH } private val OLDEST_FOR_REAL_TIME = 1.minutes 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 6b4c241a..6425c221 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 @@ -175,7 +175,8 @@ object GTFSRealTimeVehiclePositionsProvider : MTLog.Loggable { // Best practices: 90 seconds // https://gtfs.org/documentation/realtime/realtime-best-practices/#feed-publishing-general-practices - private val VEHICLE_POSITIONS_MAX_AGE_MS = 90.times(3).seconds.inWholeMilliseconds + // Montréal-based competitor: 3 minutes + private val VEHICLE_POSITIONS_MAX_AGE_MS = 90.times(2).seconds.inWholeMilliseconds private fun GTFSRealTimeProvider.loadAgencyDataFromWWW(): List? { try { 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 56245a93..7969c342 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 @@ -1201,7 +1201,6 @@ class GTFSRealTimeTripUpdatesProviderTests { @Test fun test_match_trip_id_only_matches() { - var tripIdsOutOfSync = false tripDescriptor { tripId = TRIP_ID }.match( @@ -1211,16 +1210,13 @@ class GTFSRealTimeTripUpdatesProviderTests { ignoreDirection = false, parseRouteId = { it.optRouteId }, parseTripId = { it.optTripId }, - setTripIdsOutOfSync = { tripIdsOutOfSync = it} ).let { result -> assertTrue(result) - assertFalse(tripIdsOutOfSync) } } @Test fun test_match_trip_id_only_does_not_match() { - var tripIdsOutOfSync = false tripDescriptor { tripId = TRIP_ID }.match( @@ -1230,16 +1226,13 @@ class GTFSRealTimeTripUpdatesProviderTests { ignoreDirection = false, parseRouteId = { it.optRouteId }, parseTripId = { it.optTripId }, - setTripIdsOutOfSync = { tripIdsOutOfSync = it} ).let { result -> assertFalse(result) - assertFalse(tripIdsOutOfSync) } } @Test fun test_match_trip_id_does_not_match_and_route_matches() { - var tripIdsOutOfSync = false tripDescriptor { routeId = "1" tripId = TRIP_ID @@ -1250,10 +1243,8 @@ class GTFSRealTimeTripUpdatesProviderTests { ignoreDirection = false, parseRouteId = { it.optRouteId }, parseTripId = { it.optTripId }, - setTripIdsOutOfSync = { tripIdsOutOfSync = it} ).let { result -> assertFalse(result) - assertTrue(tripIdsOutOfSync) } } From 54714ac748dcc7f15d879dd3b333800c7cc84c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 30 Jul 2026 11:50:38 -0400 Subject: [PATCH 2/9] wip --- .../status/GTFSRealTimeTripUpdatesProvider.kt | 35 ++++++++++++++----- .../GTFSRealTimeTripUpdatesProviderTests.kt | 3 +- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt index c32d19eb..9b4f6d63 100644 --- a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt +++ b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt @@ -23,11 +23,14 @@ 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.optDirectionIdValid import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduledTimeMs +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.optTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestampMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripIdNotEmpty +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optVehicle 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 @@ -57,6 +60,7 @@ import com.google.transit.realtime.GtfsRealtime.FeedMessage as GFeedMessage 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 object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { @@ -117,14 +121,19 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { return false // not useful (too old to display) } } - optStopTimeUpdateList?.firstOrNull()?.let { firstStu -> - if (firstStu.optDeparture?.hasDelay() == true || firstStu.optArrival?.hasDelay() == true) return@let - val timeMs = firstStu.optDeparture?.optTimeMs ?: firstStu.optArrival?.optTimeMs ?: return@let - if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { - MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(timeMs - nowMs).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") - return false // not useful (too far in advance to display) + optStopTimeUpdateList + ?.takeUnless { optVehicle?.hasId() == true } // SKIP FUTURE CHECK IF vehicle info provided + ?.sortedBy { it.optStopSequence } + ?.firstOrNull() + ?.takeUnless { it.optDeparture?.hasDelay() == true || it.optArrival?.hasDelay() == true } // SKIP FUTURE CHECK IF delay available + ?.takeUnless { it.optDeparture?.hasTimeDelay() == true || it.optArrival?.hasTimeDelay() == true } // SKIP FUTURE CHECK IF computed time delay available + ?.let { firstStu -> + val timeMs = firstStu.optDeparture?.optTimeMs ?: firstStu.optArrival?.optTimeMs ?: return@let + if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { + MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(timeMs - nowMs).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") + return false // not useful (too far in advance to display) + } } - } if (optDelay != null || optStopTimeUpdateList?.isNotEmpty() == true ) { @@ -139,6 +148,16 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { return false // not useful } + private fun GTUStopTimeEvent.hasTimeDelay() = + timeDelayMs?.takeIf { it != 0L } != null + + private val GTUStopTimeEvent.timeDelayMs: Long? + get() { + val timeMs = this.optTimeMs ?: return null + val scheduledTimeMs = this.optScheduledTimeMs ?: return null + return (timeMs - scheduledTimeMs) + } + // Best practices: 90 seconds // https://gtfs.org/documentation/realtime/realtime-best-practices/#feed-publishing-general-practices // Montréal-based competitor: 10 minutes @@ -232,7 +251,7 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { rdTripUpdates.forEach { (_, gTripUpdate) -> MTLog.d(LOG_TAG, "makeCachedStatusFromAgencyData() > GTFS - ${gTripUpdate.toStringExt()}.") if (PRINT_ALL_RD_STOP_TIME_UPDATES) { - gTripUpdate.stopTimeUpdateList?.forEachIndexed { idx, stu -> + gTripUpdate.optStopTimeUpdateList?.forEachIndexed { idx, stu -> MTLog.d(LOG_TAG, "makeCachedStatusFromAgencyData() > GTFS - [$idx] ${stu.toStringExt()}") } } 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 7969c342..b0b46f32 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 @@ -18,6 +18,7 @@ import org.mtransit.android.commons.data.toScheduleTimestamp import org.mtransit.android.commons.provider.gtfs.GTFSStatusProvider import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.delayDuration import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optRouteId +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStopTimeUpdateList import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripId import org.mtransit.android.commons.provider.status.GTFSRealTimeTripUpdatesProvider.match import org.mtransit.android.commons.secsToInstant @@ -1163,7 +1164,7 @@ class GTFSRealTimeTripUpdatesProviderTests { } }.sortedBy { (_, stopSequence) -> stopSequence } - val result = gTripUpdate.stopTimeUpdateList?.fixStopSequence( + val result = gTripUpdate.optStopTimeUpdateList?.fixStopSequence( tripId = TRIP_ID, tripSortedRDS = rdsList, sortedTargetUuidAndSequence = sortedTargetUuidAndSequence, From f6ba7e1002eca783205465ce53e6eabb0122b3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 30 Jul 2026 15:24:19 -0400 Subject: [PATCH 3/9] wip --- .../provider/CleverDevicesProvider.java | 2 +- .../provider/GTFSRealTimeProvider.java | 14 ++--- .../commons/provider/NextBusProvider.java | 2 +- .../commons/provider/OneBusAwayProvider.java | 2 +- .../commons/provider/RSSNewsProvider.java | 2 +- .../commons/provider/agency/AgencyUtils.kt | 2 +- .../provider/gtfs/GTFSDateTimeUtils.kt | 44 ++++++++++++++ .../provider/gtfs/GTFSRealTimeProviderExt.kt | 5 +- .../gtfs/GTFSScheduleTimestampsProvider.java | 2 +- .../provider/gtfs/GTFSStatusProvider.java | 12 ++-- .../status/GTFSRealTimeTripUpdatesProvider.kt | 59 +++++++++++-------- 11 files changed, 100 insertions(+), 46 deletions(-) create mode 100644 src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt diff --git a/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java b/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java index 164a0e9d..dbfb9cd5 100644 --- a/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/CleverDevicesProvider.java @@ -276,7 +276,7 @@ private void loadRealTimeStatusFromWWW(@NonNull RouteDirectionStop rds) { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); XMLReader xr = sp.getXMLReader(); - CleverDevicesPredictionsDataHandler handler = new CleverDevicesPredictionsDataHandler(this, newLastUpdateInMs, AgencyUtils.getRDSAgencyTimeZoneId(context), sourceLabel, rds); + CleverDevicesPredictionsDataHandler handler = new CleverDevicesPredictionsDataHandler(this, newLastUpdateInMs, AgencyUtils.getAgencyTimeZoneId(context), sourceLabel, rds); xr.setContentHandler(handler); xr.parse(new InputSource(httpUrlConnection.getInputStream())); Collection statuses = handler.getStatuses(); 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 cb46bf27..6804093a 100644 --- a/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/GTFSRealTimeProvider.java @@ -577,17 +577,17 @@ private static String getAGENCY_TIME_AM_PM_FORMAT(@NonNull Context context) { } @Nullable - private static String agencyTimeZone = null; + private static String agencyTimeZoneId = null; /** * Override if multiple {@link GTFSRealTimeProvider} implementations in same app. */ @NonNull - public static String getAGENCY_TIME_ZONE(@NonNull Context context) { - if (agencyTimeZone == null) { - agencyTimeZone = context.getResources().getString(R.string.gtfs_real_time_agency_time_zone); + public static String getAGENCY_TIME_ZONE_ID(@NonNull Context context) { + if (agencyTimeZoneId == null) { + agencyTimeZoneId = context.getResources().getString(R.string.gtfs_real_time_agency_time_zone); } - return agencyTimeZone; + return agencyTimeZoneId; } @Override @@ -1244,9 +1244,9 @@ private ThreadSafeDateFormatter getTimeParser(@NonNull Context context) { formatter += StringUtils.SPACE_STRING + getAGENCY_TIME_AM_PM_FORMAT(context); } timeParser = new ThreadSafeDateFormatter(formatter, Locale.ENGLISH); - String agencyTimeZoneId = getAGENCY_TIME_ZONE(context); + String agencyTimeZoneId = getAGENCY_TIME_ZONE_ID(context); if (TextUtils.isEmpty(agencyTimeZoneId)) { - agencyTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + agencyTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); } if (!TextUtils.isEmpty(agencyTimeZoneId)) { timeParser.setTimeZone(TimeZone.getTimeZone(agencyTimeZoneId)); diff --git a/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java b/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java index 1b8bac5a..992dc5e5 100644 --- a/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/NextBusProvider.java @@ -1072,7 +1072,7 @@ private void loadPredictionsFromWWW(@NonNull Context context, @NonNull RouteDire final SAXParserFactory spf = SAXParserFactory.newInstance(); final SAXParser sp = spf.newSAXParser(); final XMLReader xr = sp.getXMLReader(); - final NextBusPredictionsDataHandler handler = new NextBusPredictionsDataHandler(this, sourceLabel, newLastUpdateInMs, AgencyUtils.getRDSAgencyTimeZoneId(context)); + final NextBusPredictionsDataHandler handler = new NextBusPredictionsDataHandler(this, sourceLabel, newLastUpdateInMs, AgencyUtils.getAgencyTimeZoneId(context)); xr.setContentHandler(handler); xr.parse(new InputSource(response.body().byteStream())); final Collection statuses = handler.getStatuses(); diff --git a/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java b/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java index 496ae731..2f41b960 100644 --- a/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/OneBusAwayProvider.java @@ -358,7 +358,7 @@ private void loadPredictionsFromWWW(@NonNull Context context, @NonNull RouteDire private Collection parseAgencyJSON(@NonNull Context context, @Nullable String jsonString, @NonNull RouteDirectionStop rds, @Nullable String sourceLabel, long newLastUpdateInMs) { try { - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); ArrayList result = new ArrayList<>(); JSONObject json = jsonString == null ? null : new JSONObject(jsonString); if (json != null && json.has(JSON_DATA)) { diff --git a/src/main/java/org/mtransit/android/commons/provider/RSSNewsProvider.java b/src/main/java/org/mtransit/android/commons/provider/RSSNewsProvider.java index 5beafb66..2334da1d 100644 --- a/src/main/java/org/mtransit/android/commons/provider/RSSNewsProvider.java +++ b/src/main/java/org/mtransit/android/commons/provider/RSSNewsProvider.java @@ -564,7 +564,7 @@ private ArrayList loadAgencyNewsDataFromWWW(@NonNull Context context, Stri final boolean ignoreGUID = RssNewProviderUtils.pickIgnoreGUID(context, i); final boolean ignoreLink = RssNewProviderUtils.pickIgnoreLink(context, i); final Pair dateLinkFallback = RssNewProviderUtils.pickDateLinkFallback(context, i); - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); final RSSDataHandler handler = new RSSDataHandler( url, authority, diff --git a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt index 265cef8c..daf73d21 100644 --- a/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/agency/AgencyUtils.kt @@ -35,7 +35,7 @@ object AgencyUtils { private val _defaultTimeZoneId: String by lazy { TimeZone.getDefault().id } @JvmStatic - fun getRDSAgencyTimeZoneId(context: Context) = + fun getAgencyTimeZoneId(context: Context) = context.getAgencyString( R.string.poi_agency_timezone, R.string.gtfs_rts_timezone, // do not change to avoid breaking compat w/ old modules diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt new file mode 100644 index 00000000..a9aa7913 --- /dev/null +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt @@ -0,0 +1,44 @@ +package org.mtransit.android.commons.provider.gtfs + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.atStartOfDayIn +import org.mtransit.android.commons.MTLog +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant +import kotlinx.datetime.TimeZone as KtTimeZone + +object GTFSDateTimeUtils { + + fun parseToDateTime(gtfsDateStr: String, gtfsTimeStr: String, agencyTimeZone: KtTimeZone): Instant? { + try { + val cleanedDate = gtfsDateStr.trim() + if (cleanedDate.length != 8) { + MTLog.w(this, "Invalid GTFS date format '$cleanedDate'! Must be YYYYMMDD") + return null + } + val parts = gtfsTimeStr.trim().split(":") + if (parts.size != 3) { + MTLog.w(this, "Invalid GTFS time format '$gtfsTimeStr'! Must be HH:MM:SS") + return null + } + + val hoursCount = parts.getOrNull(0)?.toIntOrNull() ?: return null + val minutesCount = parts.getOrNull(1)?.toIntOrNull() ?: return null + val secondsCount = parts.getOrNull(2)?.toIntOrNull() ?: return null + val durationFromStartOfDay = hoursCount.hours + // compat with 24+ hours + minutesCount.minutes + secondsCount.seconds + + val year = cleanedDate.substring(0, 4).toIntOrNull() ?: return null + val month = cleanedDate.substring(4, 6).toIntOrNull() ?: return null + val day = cleanedDate.substring(6, 8).toIntOrNull() ?: return null + val startOfDay = LocalDate(year, month, day).atStartOfDayIn(agencyTimeZone) + + return startOfDay + durationFromStartOfDay + } catch (e: Exception) { + MTLog.w(this, e, "Error while parsing GTFS date '$gtfsDateStr' & time '$gtfsTimeStr'!") + return null + } + } +} 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 c9fd838d..fea5bdc1 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 @@ -11,7 +11,7 @@ import org.mtransit.android.commons.data.Stop import org.mtransit.android.commons.data.toRouteDirection import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.MT_HASH_SECRET_AND_DATE -import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_TIME_ZONE +import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_TIME_ZONE_ID import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_URL_HEADER_NAMES import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_URL_HEADER_VALUES import org.mtransit.android.commons.provider.GTFSRealTimeProvider.getAGENCY_URL_TOKEN @@ -39,7 +39,8 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate as GTU val Int.isValidDirection get() = this in 0..1 val GTFSRealTimeProvider.ignoreDirection get() = isIGNORE_DIRECTION(requireContextCompat()) val GTFSRealTimeProvider.targetAuthority get() = getTARGET_AUTHORITY(requireContextCompat()) -val GTFSRealTimeProvider.timeZone get() = getAGENCY_TIME_ZONE(requireContextCompat()) +val GTFSRealTimeProvider.timeZoneId get() = getAGENCY_TIME_ZONE_ID(requireContextCompat()) +val GTFSRealTimeProvider.optTimeZoneId get() = timeZoneId.takeIf { it.isNotBlank() } fun GTFSRealTimeProvider.parseAgencyId(es: GEntitySelector) = es.optAgencyIdNotEmpty?.let { parseAgencyId(it) } fun GTFSRealTimeProvider.parseAgencyId(gAgencyId: String) = gAgencyId.originalIdToId(agencyIdCleanupPattern) 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 58d6658d..3363bc62 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 @@ -49,7 +49,7 @@ public static ScheduleTimestamps getScheduleTimestamps(@NonNull GTFSProvider pro final Context context = provider.requireContextCompat(); final ThreadSafeDateFormatter dateFormat = GTFSStatusProvider.getDateFormat(context); final ThreadSafeDateFormatter timeFormat = GTFSStatusProvider.getTimeFormat(context); - final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getRDSAgencyTimeZoneId(context)); + final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getAgencyTimeZoneId(context)); final Calendar startsAt = TimeUtils.getNewCalendar(timeZone, startsAtInMs); startsAt.add(Calendar.DATE, -1); // starting yesterday Set dayTimestamps; 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 1edc26f6..8854b45a 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 @@ -175,7 +175,7 @@ public static POIStatus getNewStatus(@NonNull GTFSProvider provider, @NonNull St static ThreadSafeDateFormatter getDateFormat(@NonNull Context context) { if (dateFormat == null) { dateFormat = new ThreadSafeDateFormatter(DATE_FORMAT_PATTERN, Locale.ENGLISH); - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); dateFormat.setTimeZone(TimeZone.getTimeZone(localTimeZoneId)); } return dateFormat; @@ -189,7 +189,7 @@ static ThreadSafeDateFormatter getDateFormat(@NonNull Context context) { static ThreadSafeDateFormatter getTimeFormat(@NonNull Context context) { if (timeFormat == null) { timeFormat = new ThreadSafeDateFormatter(TIME_FORMAT_PATTERN, Locale.ENGLISH); - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); timeFormat.setTimeZone(TimeZone.getTimeZone(localTimeZoneId)); } return timeFormat; @@ -245,7 +245,7 @@ private static ArrayList findTimestamps(@NonNull GTFSProvide final Context context = provider.requireContextCompat(); final ThreadSafeDateFormatter dateFormat = getDateFormat(context); final ThreadSafeDateFormatter timeFormat = getTimeFormat(context); - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); final TimeZone timeZone = TimeZone.getTimeZone(localTimeZoneId); final Calendar startsAt = TimeUtils.getNewCalendar(timeZone, timestamp); if (lookBehindInMs > PROVIDER_PRECISION_IN_MS) { @@ -436,7 +436,7 @@ static Set findScheduleList( BufferedReader br = null; String line = null; final Context context = provider.requireContextCompat(); - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); String fileName = String.format(getSTOP_SCHEDULE_RAW_FILE_FORMAT(context), stopId); try { @SuppressLint("DiscouragedApi") @@ -604,7 +604,7 @@ private static ArrayList findFrequencies(@NonNull GTFSProvid final Context context = provider.requireContextCompat(); final ThreadSafeDateFormatter dateFormat = getDateFormat(context); final ThreadSafeDateFormatter timeFormat = getTimeFormat(context); - final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getRDSAgencyTimeZoneId(context)); + final TimeZone timeZone = TimeZone.getTimeZone(AgencyUtils.getAgencyTimeZoneId(context)); final Calendar startsAt = TimeUtils.getNewCalendar(timeZone, timestamp); startsAt.add(Calendar.DATE, -1); // starting yesterday HashSet dayFrequencies; @@ -766,7 +766,7 @@ private static Long convertToTimestamp(Context context, int timeInt, String date private static ThreadSafeDateFormatter getToTimestampFormat(Context context) { if (toTimestampFormat == null) { toTimestampFormat = new ThreadSafeDateFormatter(TO_TIMESTAMP_FORMAT_PATTERN, Locale.ENGLISH); - final String localTimeZoneId = AgencyUtils.getRDSAgencyTimeZoneId(context); + final String localTimeZoneId = AgencyUtils.getAgencyTimeZoneId(context); toTimestampFormat.setTimeZone(TimeZone.getTimeZone(localTimeZoneId)); } return toTimestampFormat; 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 9b4f6d63..2d96ce11 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 @@ -18,12 +18,15 @@ import org.mtransit.android.commons.data.makeSchedule import org.mtransit.android.commons.data.toNoData import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.ALLOW_IGNORE_TRIP_DESCRIPTOR_DIRECTION_ID +import org.mtransit.android.commons.provider.agency.AgencyUtils +import org.mtransit.android.commons.provider.gtfs.GTFSDateTimeUtils 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.optDirectionIdValid import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduledTimeMs +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartDate +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartTime 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.optTimeMs @@ -39,9 +42,11 @@ import org.mtransit.android.commons.provider.gtfs.getRDSSchedule import org.mtransit.android.commons.provider.gtfs.getTripIds import org.mtransit.android.commons.provider.gtfs.ignoreDirection import org.mtransit.android.commons.provider.gtfs.makeRequest +import org.mtransit.android.commons.provider.gtfs.optTimeZoneId import org.mtransit.android.commons.provider.gtfs.parseRouteId import org.mtransit.android.commons.provider.gtfs.parseTripId import org.mtransit.android.commons.provider.gtfs.storage +import org.mtransit.android.commons.toMillis import org.mtransit.android.toDateTimeLog import org.mtransit.android.toDurationLog import org.mtransit.commons.SourceUtils @@ -56,11 +61,12 @@ import kotlin.math.min 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 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 kotlinx.datetime.TimeZone as KtTimeZone object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { @@ -114,26 +120,28 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { private const val PRINT_ALL_RD_STOP_TIME_UPDATES = false // private const val PRINT_ALL_RD_STOP_TIME_UPDATES = true // DEBUG - private fun GTripUpdate.isUseful(nowMs: Long): Boolean { + private fun GTripUpdate.isUseful(nowMs: Long, agencyTimeZone: KtTimeZone): Boolean { optTimestampMs?.let { tuTimestamp -> if (tuTimestamp + TRIP_UPDATE_MAX_AGE_MS < nowMs) { MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(nowMs - tuTimestamp).toDurationLog()} old: ${this.optTrip?.toStringExt(true)}") return false // not useful (too old to display) } } - optStopTimeUpdateList - ?.takeUnless { optVehicle?.hasId() == true } // SKIP FUTURE CHECK IF vehicle info provided - ?.sortedBy { it.optStopSequence } - ?.firstOrNull() - ?.takeUnless { it.optDeparture?.hasDelay() == true || it.optArrival?.hasDelay() == true } // SKIP FUTURE CHECK IF delay available - ?.takeUnless { it.optDeparture?.hasTimeDelay() == true || it.optArrival?.hasTimeDelay() == true } // SKIP FUTURE CHECK IF computed time delay available - ?.let { firstStu -> - val timeMs = firstStu.optDeparture?.optTimeMs ?: firstStu.optArrival?.optTimeMs ?: return@let - if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { - MTLog.d(LOG_TAG, "isUseful() > IGNORE ${(timeMs - nowMs).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") - return false // not useful (too far in advance to display) - } + val hasVehicleInfo = optVehicle?.let { it.hasId() || it.hasLabel() || it.hasLicensePlate() } == true + val startDateTimeOrFirstTimeMs: Long? = if (hasVehicleInfo) null else + (optTrip?.optStart(agencyTimeZone)?.toMillis() + ?: optStopTimeUpdateList?.sortedBy { it.optStopSequence } + ?.firstOrNull { + it.hasDeparture() || it.hasArrival() + }?.let { + it.optDeparture?.optTimeMs ?: it.optArrival?.optTimeMs + }) + startDateTimeOrFirstTimeMs?.let { startDateTimeOrFirstTimeMs -> + if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < startDateTimeOrFirstTimeMs) { + MTLog.d(LOG_TAG, "isUseful() > IGNORE ${((startDateTimeOrFirstTimeMs - nowMs)).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") + return false // not useful (too far in advance to display) } + } if (optDelay != null || optStopTimeUpdateList?.isNotEmpty() == true ) { @@ -148,15 +156,13 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { return false // not useful } - private fun GTUStopTimeEvent.hasTimeDelay() = - timeDelayMs?.takeIf { it != 0L } != null - - private val GTUStopTimeEvent.timeDelayMs: Long? - get() { - val timeMs = this.optTimeMs ?: return null - val scheduledTimeMs = this.optScheduledTimeMs ?: return null - return (timeMs - scheduledTimeMs) - } + private fun GTripDescriptor.optStart(agencyTimeZone: KtTimeZone): Instant? { + return GTFSDateTimeUtils.parseToDateTime( + this.optStartDate ?: return null, + this.optStartTime ?: return null, + agencyTimeZone + ) + } // Best practices: 90 seconds // https://gtfs.org/documentation/realtime/realtime-best-practices/#feed-publishing-general-practices @@ -204,6 +210,9 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { val rdSchedules: Collection by lazy { context.getRDSSchedule(targetAuthority, sortedRDS, filter.isIncludeCancelledTimestampsOrDefault) } + val agencyTimeZoneId = this.optTimeZoneId + ?: AgencyUtils.getAgencyTimeZoneId(context) + val agencyTimeZone = KtTimeZone.of(agencyTimeZoneId) val rdTripUpdates = gTripUpdates .filter { gTripUpdate -> gTripUpdate.optTrip?.match( @@ -215,7 +224,7 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { parseTripId = ::parseTripId, ) == true } - .filter { it.isUseful(nowMs) } + .filter { it.isUseful(nowMs, agencyTimeZone) } .filterDuplicatesTrips() .takeIf { it.isNotEmpty() } ?.mapNotNull { gTripUpdate -> gTripUpdate.optTrip?.let { it to gTripUpdate } } From 8a5db6b825b055d3735e23ebb6bd1669eb1e7f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 30 Jul 2026 15:29:22 -0400 Subject: [PATCH 4/9] wip --- .../provider/gtfs/GTFSDateTimeUtils.kt | 12 +++++++---- .../status/GTFSRealTimeTripUpdatesProvider.kt | 21 +++++++------------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt index a9aa7913..ce663082 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt @@ -9,16 +9,20 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant import kotlinx.datetime.TimeZone as KtTimeZone -object GTFSDateTimeUtils { +object GTFSDateTimeUtils : MTLog.Loggable { - fun parseToDateTime(gtfsDateStr: String, gtfsTimeStr: String, agencyTimeZone: KtTimeZone): Instant? { + private val LOG_TAG: String = GTFSDateTimeUtils::class.java.simpleName + + override fun getLogTag() = LOG_TAG + + fun parseToDateTime(gtfsDateStr: String?, gtfsTimeStr: String?, agencyTimeZone: KtTimeZone): Instant? { try { - val cleanedDate = gtfsDateStr.trim() + val cleanedDate = gtfsDateStr?.trim() ?: return null if (cleanedDate.length != 8) { MTLog.w(this, "Invalid GTFS date format '$cleanedDate'! Must be YYYYMMDD") return null } - val parts = gtfsTimeStr.trim().split(":") + val parts = gtfsTimeStr?.trim()?.split(":") ?: return null if (parts.size != 3) { MTLog.w(this, "Invalid GTFS time format '$gtfsTimeStr'! Must be HH:MM:SS") return 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 2d96ce11..5186f3db 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 @@ -19,11 +19,12 @@ import org.mtransit.android.commons.data.toNoData import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.ALLOW_IGNORE_TRIP_DESCRIPTOR_DIRECTION_ID import org.mtransit.android.commons.provider.agency.AgencyUtils -import org.mtransit.android.commons.provider.gtfs.GTFSDateTimeUtils +import org.mtransit.android.commons.provider.gtfs.GTFSDateTimeUtils.parseToDateTime 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.optDirectionIdValid +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optModifiedTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartDate import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartTime @@ -61,7 +62,6 @@ import kotlin.math.min 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 as GTripDescriptor import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship as GTDScheduleRelationship @@ -129,16 +129,17 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { } val hasVehicleInfo = optVehicle?.let { it.hasId() || it.hasLabel() || it.hasLicensePlate() } == true val startDateTimeOrFirstTimeMs: Long? = if (hasVehicleInfo) null else - (optTrip?.optStart(agencyTimeZone)?.toMillis() + (optTrip?.let { parseToDateTime(it.optStartDate, it.optStartTime, agencyTimeZone) }?.toMillis() + ?: optTrip?.optModifiedTrip?.let { parseToDateTime(it.optStartDate, it.optStartTime, agencyTimeZone) }?.toMillis() ?: optStopTimeUpdateList?.sortedBy { it.optStopSequence } ?.firstOrNull { it.hasDeparture() || it.hasArrival() }?.let { it.optDeparture?.optTimeMs ?: it.optArrival?.optTimeMs }) - startDateTimeOrFirstTimeMs?.let { startDateTimeOrFirstTimeMs -> - if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < startDateTimeOrFirstTimeMs) { - MTLog.d(LOG_TAG, "isUseful() > IGNORE ${((startDateTimeOrFirstTimeMs - nowMs)).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") + startDateTimeOrFirstTimeMs?.let { timeMs -> + if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { + MTLog.d(LOG_TAG, "isUseful() > IGNORE ${((timeMs - nowMs)).toDurationLog()} in the future: ${this.optTrip?.toStringExt(true)}") return false // not useful (too far in advance to display) } } @@ -156,14 +157,6 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { return false // not useful } - private fun GTripDescriptor.optStart(agencyTimeZone: KtTimeZone): Instant? { - return GTFSDateTimeUtils.parseToDateTime( - this.optStartDate ?: return null, - this.optStartTime ?: return null, - agencyTimeZone - ) - } - // Best practices: 90 seconds // https://gtfs.org/documentation/realtime/realtime-best-practices/#feed-publishing-general-practices // Montréal-based competitor: 10 minutes From 94869fbc5472115873844e3c2c7c6224fbf58ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 30 Jul 2026 15:33:03 -0400 Subject: [PATCH 5/9] fix --- .../android/commons/provider/gtfs/GTFSDateTimeUtils.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt index ce663082..27841c51 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt @@ -28,9 +28,9 @@ object GTFSDateTimeUtils : MTLog.Loggable { return null } - val hoursCount = parts.getOrNull(0)?.toIntOrNull() ?: return null - val minutesCount = parts.getOrNull(1)?.toIntOrNull() ?: return null - val secondsCount = parts.getOrNull(2)?.toIntOrNull() ?: return null + val hoursCount = parts.getOrNull(0)?.toIntOrNull()?.takeIf { it > 0 } ?: return null + val minutesCount = parts.getOrNull(1)?.toIntOrNull()?.takeIf { it in 0..59 } ?: return null + val secondsCount = parts.getOrNull(2)?.toIntOrNull()?.takeIf { it in 0..59 } ?: return null val durationFromStartOfDay = hoursCount.hours + // compat with 24+ hours minutesCount.minutes + secondsCount.seconds From 4c4f6b33f1a99b40f4f11616c8cf952e68159d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Thu, 30 Jul 2026 15:41:43 -0400 Subject: [PATCH 6/9] tests --- .../provider/gtfs/GTFSDateTimeUtilsTest.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt diff --git a/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt b/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt new file mode 100644 index 00000000..6f3fc0d0 --- /dev/null +++ b/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt @@ -0,0 +1,38 @@ +package org.mtransit.android.commons.provider.gtfs + +import org.mtransit.android.commons.millisToInstant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlinx.datetime.TimeZone as KtTimeZone + +class GTFSDateTimeUtilsTest { + + private val timeZone = KtTimeZone.of("America/Edmonton") + + @Test + fun test_parseToDateTime() { + ("20260730" to "12:24:00").let { (date, time) -> + GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) + }.let { result -> + assertEquals(1785435840_000L.millisToInstant(), result) + } + ("20260730" to "24:24:00").let { (date, time) -> + GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) + }.let { result -> + println(result?.toEpochMilliseconds()) + assertEquals(1785479040_000L.millisToInstant(), result) + } + (null to null).let { (date, time) -> + GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) + }.let { result -> + assertNull(result) + } + ("2026-07-30" to "-1:74:77").let { (date, time) -> + GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) + }.let { result -> + assertNull(result) + } + } + +} From 0c60401a2e4073f3214e4198b2264b371229f9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 31 Jul 2026 08:46:56 -0400 Subject: [PATCH 7/9] fixes --- .../commons/provider/gtfs/GTFSDateTimeUtils.kt | 4 ++-- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 5 ++++- .../status/GTFSRealTimeTripUpdatesProvider.kt | 18 +++++++++++------- .../provider/gtfs/GTFSDateTimeUtilsTest.kt | 6 +++++- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt index 27841c51..20c375b8 100644 --- a/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt +++ b/src/main/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtils.kt @@ -18,7 +18,7 @@ object GTFSDateTimeUtils : MTLog.Loggable { fun parseToDateTime(gtfsDateStr: String?, gtfsTimeStr: String?, agencyTimeZone: KtTimeZone): Instant? { try { val cleanedDate = gtfsDateStr?.trim() ?: return null - if (cleanedDate.length != 8) { + if (cleanedDate.length != 8 || cleanedDate.any { !it.isDigit() }) { MTLog.w(this, "Invalid GTFS date format '$cleanedDate'! Must be YYYYMMDD") return null } @@ -28,7 +28,7 @@ object GTFSDateTimeUtils : MTLog.Loggable { return null } - val hoursCount = parts.getOrNull(0)?.toIntOrNull()?.takeIf { it > 0 } ?: return null + val hoursCount = parts.getOrNull(0)?.toIntOrNull()?.takeIf { it >= 0 } ?: return null val minutesCount = parts.getOrNull(1)?.toIntOrNull()?.takeIf { it in 0..59 } ?: return null val secondsCount = parts.getOrNull(2)?.toIntOrNull()?.takeIf { it in 0..59 } ?: return null val durationFromStartOfDay = hoursCount.hours + // compat with 24+ hours 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 d5525661..40a19242 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 @@ -226,7 +226,8 @@ object GtfsRealtimeExt { val GTripUpdate.optTrip get() = if (hasTrip()) trip else null val GTripUpdate.optVehicle get() = if (hasVehicle()) vehicle else null - val GTripUpdate.optStopTimeUpdateList get() = stopTimeUpdateList?.takeIf { it.isNotEmpty() } + fun GTripUpdate.hasStopTimeUpdateList() = stopTimeUpdateList.isNotEmpty() + val GTripUpdate.optStopTimeUpdateList get() = if (hasStopTimeUpdateList()) stopTimeUpdateList else null val GTripUpdate.optTimestamp get() = if (hasTimestamp()) timestamp else null val GTripUpdate.optTimestampMs get() = optTimestamp?.secToMs() val GTripUpdate.optDelay get() = if (hasDelay()) delay else null @@ -299,6 +300,8 @@ object GtfsRealtimeExt { val GTUStopTimeEvent.optScheduledTime get() = if (hasScheduledTime()) scheduledTime else null val GTUStopTimeEvent.optScheduledTimeMs get() = optScheduledTime?.secToMs() val GTUStopTimeEvent.optScheduledTimeInstant get() = optScheduledTime?.secsToInstant() + val GTUStopTimeEvent.optTimeOrScheduleTime get() = optTime ?: optScheduledTime + val GTUStopTimeEvent.optTimeOrScheduleTimeMs get() = optTimeOrScheduleTime?.secToMs() @JvmStatic @JvmOverloads 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 5186f3db..4f892082 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 @@ -20,17 +20,20 @@ import org.mtransit.android.commons.provider.GTFSRealTimeProvider import org.mtransit.android.commons.provider.GTFSRealTimeProvider.ALLOW_IGNORE_TRIP_DESCRIPTOR_DIRECTION_ID import org.mtransit.android.commons.provider.agency.AgencyUtils import org.mtransit.android.commons.provider.gtfs.GTFSDateTimeUtils.parseToDateTime +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.hasStopTimeUpdateList 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.optDirectionIdValid import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optModifiedTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduledTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartDate import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartTime 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.optTimeMs +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeOrScheduleTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestampMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripIdNotEmpty @@ -133,9 +136,9 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { ?: optTrip?.optModifiedTrip?.let { parseToDateTime(it.optStartDate, it.optStartTime, agencyTimeZone) }?.toMillis() ?: optStopTimeUpdateList?.sortedBy { it.optStopSequence } ?.firstOrNull { - it.hasDeparture() || it.hasArrival() + it.optDeparture?.optTimeOrScheduleTimeMs != null || it.optArrival?.optTimeOrScheduleTimeMs != null }?.let { - it.optDeparture?.optTimeMs ?: it.optArrival?.optTimeMs + it.optDeparture?.optTimeOrScheduleTimeMs ?: it.optArrival?.optTimeOrScheduleTimeMs }) startDateTimeOrFirstTimeMs?.let { timeMs -> if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { @@ -143,10 +146,8 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { return false // not useful (too far in advance to display) } } - if (optDelay != null - || optStopTimeUpdateList?.isNotEmpty() == true - ) { - return true // useful + if (hasDelay() || hasStopTimeUpdateList()) { + return true // useful (contains delay or STUs) } optTrip?.let { td -> // cannot match w/ static data if (td.optScheduleRelationship?.let { it != GTDScheduleRelationship.SCHEDULED } == true) { @@ -205,7 +206,10 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { } val agencyTimeZoneId = this.optTimeZoneId ?: AgencyUtils.getAgencyTimeZoneId(context) - val agencyTimeZone = KtTimeZone.of(agencyTimeZoneId) + val agencyTimeZone = runCatching { KtTimeZone.of(agencyTimeZoneId) }.getOrElse { e -> + MTLog.w(LOG_TAG, e, "makeCachedStatusFromAgencyData() > error getting timezone from '$agencyTimeZoneId'!") + KtTimeZone.currentSystemDefault() + } val rdTripUpdates = gTripUpdates .filter { gTripUpdate -> gTripUpdate.optTrip?.match( diff --git a/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt b/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt index 6f3fc0d0..fb4728d4 100644 --- a/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt +++ b/src/test/java/org/mtransit/android/commons/provider/gtfs/GTFSDateTimeUtilsTest.kt @@ -20,9 +20,13 @@ class GTFSDateTimeUtilsTest { ("20260730" to "24:24:00").let { (date, time) -> GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) }.let { result -> - println(result?.toEpochMilliseconds()) assertEquals(1785479040_000L.millisToInstant(), result) } + ("20260730" to "00:24:00").let { (date, time) -> + GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) + }.let { result -> + assertEquals(1785392640_000L.millisToInstant(), result) + } (null to null).let { (date, time) -> GTFSDateTimeUtils.parseToDateTime(date, time, timeZone) }.let { result -> From f926e66b9fa891e021ae03a56daad9b6c09a705e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 31 Jul 2026 09:45:34 -0400 Subject: [PATCH 8/9] PR comments --- .../commons/provider/gtfs/GtfsRealtimeExt.kt | 5 +++-- .../status/GTFSRealTimeTripUpdatesProvider.kt | 15 ++++++--------- 2 files changed, 9 insertions(+), 11 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 40a19242..e3c70295 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 @@ -300,8 +300,9 @@ object GtfsRealtimeExt { val GTUStopTimeEvent.optScheduledTime get() = if (hasScheduledTime()) scheduledTime else null val GTUStopTimeEvent.optScheduledTimeMs get() = optScheduledTime?.secToMs() val GTUStopTimeEvent.optScheduledTimeInstant get() = optScheduledTime?.secsToInstant() - val GTUStopTimeEvent.optTimeOrScheduleTime get() = optTime ?: optScheduledTime - val GTUStopTimeEvent.optTimeOrScheduleTimeMs get() = optTimeOrScheduleTime?.secToMs() + fun GTUStopTimeEvent.hasTimeOrScheduledTime() = hasTime() || hasScheduledTime() + val GTUStopTimeEvent.optTimeOrScheduledTime get() = optTime ?: optScheduledTime + val GTUStopTimeEvent.optTimeOrScheduledTimeMs get() = optTimeOrScheduledTime?.secToMs() @JvmStatic @JvmOverloads 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 4f892082..74002fc8 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 @@ -21,19 +21,17 @@ import org.mtransit.android.commons.provider.GTFSRealTimeProvider.ALLOW_IGNORE_T import org.mtransit.android.commons.provider.agency.AgencyUtils import org.mtransit.android.commons.provider.gtfs.GTFSDateTimeUtils.parseToDateTime import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.hasStopTimeUpdateList +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.hasTimeOrScheduledTime 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.optDirectionIdValid import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optModifiedTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduledTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartDate import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartTime 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.optTimeMs -import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeOrScheduleTimeMs +import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimeOrScheduledTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestampMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTrip import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTripIdNotEmpty @@ -134,11 +132,10 @@ object GTFSRealTimeTripUpdatesProvider : MTLog.Loggable { val startDateTimeOrFirstTimeMs: Long? = if (hasVehicleInfo) null else (optTrip?.let { parseToDateTime(it.optStartDate, it.optStartTime, agencyTimeZone) }?.toMillis() ?: optTrip?.optModifiedTrip?.let { parseToDateTime(it.optStartDate, it.optStartTime, agencyTimeZone) }?.toMillis() - ?: optStopTimeUpdateList?.sortedBy { it.optStopSequence } - ?.firstOrNull { - it.optDeparture?.optTimeOrScheduleTimeMs != null || it.optArrival?.optTimeOrScheduleTimeMs != null - }?.let { - it.optDeparture?.optTimeOrScheduleTimeMs ?: it.optArrival?.optTimeOrScheduleTimeMs + ?: optStopTimeUpdateList // not sorting because GTFS spec requires it to be already sorted & it's a fallback + ?.firstOrNull { it.optDeparture?.hasTimeOrScheduledTime() == true || it.optArrival?.hasTimeOrScheduledTime() == true } + ?.let { + it.optDeparture?.optTimeOrScheduledTimeMs ?: it.optArrival?.optTimeOrScheduledTimeMs }) startDateTimeOrFirstTimeMs?.let { timeMs -> if (nowMs + FUTURE_TRIP_UPDATE_MAX_DIFF_MS < timeMs) { From 102dae8ca0498210b67006b1213eab64f728a43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Fri, 31 Jul 2026 10:28:07 -0400 Subject: [PATCH 9/9] import --- .../commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt b/src/main/java/org/mtransit/android/commons/provider/status/GTFSRealTimeTripUpdatesProvider.kt index 74002fc8..00b2f03a 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 @@ -29,7 +29,6 @@ import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optModifiedTri import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optScheduleRelationship import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartDate import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optStartTime -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.optTimeOrScheduledTimeMs import org.mtransit.android.commons.provider.gtfs.GtfsRealtimeExt.optTimestampMs