From 9ecd7715d29d950c36554d9bf0f2c7637e102574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 10:45:15 -0400 Subject: [PATCH 1/7] GTFS Static > add stop timezone (FF: OFF) - https://github.com/mtransitapps/commons-java/pull/47 --- .../org/mtransit/parser/gtfs/GReader.java | 9 ++++--- .../org/mtransit/parser/gtfs/data/GStop.kt | 9 ++++++- .../java/org/mtransit/parser/mt/data/MStop.kt | 24 ++++++++++++------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index 13f47c1c..d161da95 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -175,8 +175,11 @@ public static GSpec readGtfsZipFile( } // STOPS (after stop times) if (!calendarsOnly && !routeTripCalendarsOnly) { + final GAgency singleAgency = gSpec.getSingleAgency(); + //noinspection DiscouragedApi + final String agencyTimezone = singleAgency == null ? null : singleAgency.getAgencyTimezone(); readFile(gtfsDir, GStop.FILENAME, true, line -> - processStop(agencyTools, gSpec, line, skipDataCleanup) + processStop(agencyTools, gSpec, line, skipDataCleanup, agencyTimezone) ); } // TODO OTHER FILES TYPE @@ -546,14 +549,14 @@ private static void processTrip( } } - private static void processStop(GAgencyTools agencyTools, GSpec gSpec, Map line, boolean skipDataCleanup) { + private static void processStop(GAgencyTools agencyTools, GSpec gSpec, Map line, boolean skipDataCleanup, @Nullable String agencyTimezone) { try { final GLocationType stopLocationType = GLocationType.parse(line.get(GStop.LOCATION_TYPE)); if (stopLocationType == GLocationType.GENERIC_NODE) { MTLog.log("Generic node stop ignored (%s).", line); // not lat/lng? return; } - final GStop gStop = skipDataCleanup ? GStop.fromLine(line) : GStop.fromLine(line, agencyTools); + final GStop gStop = skipDataCleanup ? GStop.fromLine(line, agencyTimezone) : GStop.fromLine(line, agencyTimezone, agencyTools); if (agencyTools.excludeStop(gStop)) { //noinspection DiscouragedApi logExclude("Exclude stop: %s.", line.get(GStop.STOP_ID)); diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt index 98e2eafa..06b902e9 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt @@ -18,6 +18,7 @@ data class GStop( val stopCode: String, val locationType: GLocationType, val parentStationIdInt: Int?, + val stopTimezone: String?, var wheelchairBoarding: GWheelchairBoardingType, ) { @@ -29,6 +30,7 @@ data class GStop( stopCode: String, locationType: Int?, parentStationId: StopId?, + stopTimezone: String?, wheelchairBoarding: Int?, ) : this( GIDs.getInt(stopId), @@ -38,6 +40,7 @@ data class GStop( stopCode, GLocationType.parse(locationType), parentStationId?.let { GIDs.getInt(it) }, + stopTimezone, GWheelchairBoardingType.parse(wheelchairBoarding), ) @@ -92,6 +95,7 @@ data class GStop( stopUrl = null, // TODO locationType = locationType.id, parentStationId = _parentStationId, + stopTimezone = stopTimezone, wheelchairBoarding = wheelchairBoarding.id, ) @@ -115,11 +119,12 @@ data class GStop( private const val STOP_CODE = "stop_code" internal const val LOCATION_TYPE = "location_type" private const val PARENT_STATION = "parent_station" + private const val STOP_TIMEZONE = "stop_timezone" private const val WHEELCHAIR_BOARDING = "wheelchair_boarding" @JvmOverloads @JvmStatic - fun fromLine(line: Map, agencyTools: GAgencyTools? = null) = GStop( + fun fromLine(line: Map, agencyTimezone: String?, agencyTools: GAgencyTools? = null) = GStop( stopId = line[STOP_ID]?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it } ?: throw MTLog.Fatal("Invalid GStop from $line!"), @@ -130,6 +135,7 @@ data class GStop( locationType = line[LOCATION_TYPE]?.takeIf { it.isNotBlank() }?.toInt(), parentStationId = line[PARENT_STATION]?.takeIf { it.isNotBlank() }?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it }, + stopTimezone = line[STOP_TIMEZONE]?.takeIf { it.isNotBlank() && it != agencyTimezone }, wheelchairBoarding = line[WHEELCHAIR_BOARDING]?.takeIf { it.isNotBlank() }?.toInt(), ) @@ -146,6 +152,7 @@ data class GStop( stopCode = it.stopCode ?: EMPTY, locationType = it.locationType, parentStationId = it.parentStationId, + stopTimezone = it.stopTimezone, wheelchairBoarding = it.wheelchairBoarding, ) } diff --git a/src/main/java/org/mtransit/parser/mt/data/MStop.kt b/src/main/java/org/mtransit/parser/mt/data/MStop.kt index 5ed630e9..5cd95280 100644 --- a/src/main/java/org/mtransit/parser/mt/data/MStop.kt +++ b/src/main/java/org/mtransit/parser/mt/data/MStop.kt @@ -15,6 +15,7 @@ data class MStop( val lng: Double, val accessible: Int, private val originalIdHash: Int, + val timeZoneId: String?, ) : Comparable { constructor( @@ -25,6 +26,7 @@ data class MStop( lng: Double, accessible: Int, originalId: String, + timeZoneId: String?, agencyTools: GAgencyTools? = null, ) : this( id, @@ -34,21 +36,25 @@ data class MStop( lng, accessible, GTFSCommons.stringIdToHash(originalId), + timeZoneId ) fun hasLat() = lat != 0.0 fun hasLng() = lng != 0.0 - fun toFile() = listOf( - id.toString(), // ID - code.quotesEscape(), // code - name.toStringIds(FeatureFlags.F_EXPORT_STRINGS).quotesEscape(), // name - MDataChangedManager.avoidLatLngChanged(lat), // latitude - MDataChangedManager.avoidLatLngChanged(lng), // longitude - accessible.toString(), - originalIdHash.toString(), // original ID hash - ).joinToString(SQLUtils.COLUMN_SEPARATOR) + fun toFile() = buildList { + add(id.toString()) // ID + add(code.quotesEscape()) // code + add(name.toStringIds(FeatureFlags.F_EXPORT_STRINGS).quotesEscape()) // name + add(MDataChangedManager.avoidLatLngChanged(lat)) // latitude + add(MDataChangedManager.avoidLatLngChanged(lng)) // longitude + add(accessible.toString()) + add(originalIdHash.toString()) // original ID hash + if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { + add(timeZoneId?.quotesEscape().orEmpty()) // time zone ID + } + }.joinToString(SQLUtils.COLUMN_SEPARATOR) override fun compareTo(other: MStop): Int { return id - other.id From 4040be9c9af33275aed1da0577a4269a125f99b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 11:28:30 -0400 Subject: [PATCH 2/7] wip --- src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java index 112dc1fa..c13dc1fe 100644 --- a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java +++ b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java @@ -710,6 +710,7 @@ private HashMap parseGTripStops( gStop.getStopLong(), gStop.getWheelchairBoarding().getId(), gStop.getStopId(), + gStop.getStopTimezone(), this.agencyTools )); } From 5984353bb0a70a77354445e87efb194dc52b5bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 11:40:33 -0400 Subject: [PATCH 3/7] wip --- src/main/java/org/mtransit/parser/mt/data/MStop.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/parser/mt/data/MStop.kt b/src/main/java/org/mtransit/parser/mt/data/MStop.kt index 5cd95280..8be9430c 100644 --- a/src/main/java/org/mtransit/parser/mt/data/MStop.kt +++ b/src/main/java/org/mtransit/parser/mt/data/MStop.kt @@ -52,7 +52,7 @@ data class MStop( add(accessible.toString()) add(originalIdHash.toString()) // original ID hash if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { - add(timeZoneId?.quotesEscape().orEmpty()) // time zone ID + add(timeZoneId.orEmpty().quotesEscape()) // time zone ID } }.joinToString(SQLUtils.COLUMN_SEPARATOR) From 9f20b86722899a696a24265b9c46b322e9841cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 14:03:34 -0400 Subject: [PATCH 4/7] wip --- .../java/org/mtransit/parser/gtfs/data/GAgency.kt | 5 ++++- src/main/java/org/mtransit/parser/gtfs/data/GStop.kt | 11 +++++++++-- src/main/java/org/mtransit/parser/mt/data/MStop.kt | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GAgency.kt b/src/main/java/org/mtransit/parser/gtfs/data/GAgency.kt index 33a157f1..a7a33a8d 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GAgency.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GAgency.kt @@ -4,6 +4,7 @@ import androidx.annotation.Discouraged import org.mtransit.commons.gtfs.data.Agency import org.mtransit.commons.gtfs.data.AgencyId import org.mtransit.parser.MTLog +import java.time.ZoneId // https://gtfs.org/schedule/reference/#agencytxt data class GAgency( @@ -86,7 +87,9 @@ data class GAgency( agencyId = line[AGENCY_ID].orEmpty(), agencyName = line[AGENCY_NAME] ?: throw MTLog.Fatal("Invalid GAgency from $line!"), agencyUrl = line[AGENCY_URL] ?: throw MTLog.Fatal("Invalid GAgency from $line!"), - agencyTimezone = line[AGENCY_TIMEZONE] ?: throw MTLog.Fatal("Invalid GAgency from $line!"), + agencyTimezone = line[AGENCY_TIMEZONE] + ?.takeIf { ZoneId.getAvailableZoneIds().contains(it) } + ?: throw MTLog.Fatal("Invalid GAgency from $line!"), agencyLang = line[AGENCY_LANG], agencyPhone = line[AGENCY_PHONE], agencyFareUrl = line[AGENCY_FARE_URL], diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt index 06b902e9..6da12eee 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt @@ -7,6 +7,7 @@ import org.mtransit.commons.gtfs.data.StopId import org.mtransit.parser.Constants import org.mtransit.parser.MTLog import org.mtransit.parser.gtfs.GAgencyTools +import java.time.ZoneId import kotlin.math.floor // https://gtfs.org/schedule/reference/#stopstxt @@ -135,7 +136,13 @@ data class GStop( locationType = line[LOCATION_TYPE]?.takeIf { it.isNotBlank() }?.toInt(), parentStationId = line[PARENT_STATION]?.takeIf { it.isNotBlank() }?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it }, - stopTimezone = line[STOP_TIMEZONE]?.takeIf { it.isNotBlank() && it != agencyTimezone }, + stopTimezone = line[STOP_TIMEZONE] + ?.also { gStopTimezone -> + if (!ZoneId.getAvailableZoneIds().contains(gStopTimezone)) { + throw MTLog.Fatal("Invalid stop time zone in $line!") + } + } + ?.takeIf { it.isNotBlank() && it != agencyTimezone }, wheelchairBoarding = line[WHEELCHAIR_BOARDING]?.takeIf { it.isNotBlank() }?.toInt(), ) @@ -162,4 +169,4 @@ data class GStop( return floor((loc1 + loc2) / 2.00) } } -} \ No newline at end of file +} diff --git a/src/main/java/org/mtransit/parser/mt/data/MStop.kt b/src/main/java/org/mtransit/parser/mt/data/MStop.kt index 8be9430c..3b58db18 100644 --- a/src/main/java/org/mtransit/parser/mt/data/MStop.kt +++ b/src/main/java/org/mtransit/parser/mt/data/MStop.kt @@ -4,6 +4,7 @@ import org.mtransit.commons.FeatureFlags import org.mtransit.commons.GTFSCommons import org.mtransit.commons.sql.SQLUtils import org.mtransit.parser.db.SQLUtils.quotesEscape +import org.mtransit.parser.db.SQLUtils.quotesEscapeId import org.mtransit.parser.gtfs.GAgencyTools import org.mtransit.parser.mt.MDataChangedManager @@ -52,7 +53,7 @@ data class MStop( add(accessible.toString()) add(originalIdHash.toString()) // original ID hash if (FeatureFlags.F_EXPORT_STOP_TIMEZONE_ID) { - add(timeZoneId.orEmpty().quotesEscape()) // time zone ID + add(timeZoneId.orEmpty().quotesEscapeId()) // time zone ID (can contain "_") } }.joinToString(SQLUtils.COLUMN_SEPARATOR) From 3fd924167635f19d396e8c5959f4c1d944f29f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 14:41:16 -0400 Subject: [PATCH 5/7] fix --- src/main/java/org/mtransit/parser/gtfs/data/GStop.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt index 6da12eee..20ae99d4 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt @@ -137,12 +137,13 @@ data class GStop( parentStationId = line[PARENT_STATION]?.takeIf { it.isNotBlank() }?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it }, stopTimezone = line[STOP_TIMEZONE] + ?.takeIf { it.isNotBlank() } ?.also { gStopTimezone -> if (!ZoneId.getAvailableZoneIds().contains(gStopTimezone)) { - throw MTLog.Fatal("Invalid stop time zone in $line!") + throw MTLog.Fatal("Invalid stop timezone in $line!") } } - ?.takeIf { it.isNotBlank() && it != agencyTimezone }, + ?.takeIf { it != agencyTimezone }, wheelchairBoarding = line[WHEELCHAIR_BOARDING]?.takeIf { it.isNotBlank() }?.toInt(), ) From 10524a429a2e81ccc4b203d9e70dce904820bb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 15:02:46 -0400 Subject: [PATCH 6/7] cache `ZoneId.getAvailableZoneIds()` --- src/main/java/org/mtransit/parser/gtfs/GReader.java | 8 ++++++-- src/main/java/org/mtransit/parser/gtfs/data/GAgency.kt | 7 +++---- src/main/java/org/mtransit/parser/gtfs/data/GStop.kt | 7 +++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index d161da95..98644065 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -38,6 +38,7 @@ import java.nio.file.Files; import java.sql.PreparedStatement; import java.text.DateFormat; +import java.time.ZoneId; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -415,9 +416,11 @@ private static void processFrequency( } } + private static final Set AVAILABLE_TIME_ZONE_IDS = ZoneId.getAvailableZoneIds(); // cache because it returns new set copy every time + private static void processAgency(GAgencyTools agencyTools, GSpec gSpec, HashMap line) { try { - final GAgency gAgency = GAgency.fromLine(line); + final GAgency gAgency = GAgency.fromLine(line, AVAILABLE_TIME_ZONE_IDS); if (agencyTools.excludeAgency(gAgency)) { MTLog.logDebug("processAgency() > SKIP (exclude agency)"); return; @@ -556,7 +559,8 @@ private static void processStop(GAgencyTools agencyTools, GSpec gSpec, Map) = GAgency( + fun fromLine(line: Map, availableZoneIds: Set) = GAgency( agencyId = line[AGENCY_ID].orEmpty(), agencyName = line[AGENCY_NAME] ?: throw MTLog.Fatal("Invalid GAgency from $line!"), agencyUrl = line[AGENCY_URL] ?: throw MTLog.Fatal("Invalid GAgency from $line!"), - agencyTimezone = line[AGENCY_TIMEZONE] - ?.takeIf { ZoneId.getAvailableZoneIds().contains(it) } + agencyTimezone = line[AGENCY_TIMEZONE]?.trim() + ?.takeIf { availableZoneIds.contains(it) } ?: throw MTLog.Fatal("Invalid GAgency from $line!"), agencyLang = line[AGENCY_LANG], agencyPhone = line[AGENCY_PHONE], diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt index 20ae99d4..cf6cdf18 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GStop.kt @@ -7,7 +7,6 @@ import org.mtransit.commons.gtfs.data.StopId import org.mtransit.parser.Constants import org.mtransit.parser.MTLog import org.mtransit.parser.gtfs.GAgencyTools -import java.time.ZoneId import kotlin.math.floor // https://gtfs.org/schedule/reference/#stopstxt @@ -125,7 +124,7 @@ data class GStop( @JvmOverloads @JvmStatic - fun fromLine(line: Map, agencyTimezone: String?, agencyTools: GAgencyTools? = null) = GStop( + fun fromLine(line: Map, agencyTimezone: String?, availableZoneIds: Set, agencyTools: GAgencyTools? = null) = GStop( stopId = line[STOP_ID]?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it } ?: throw MTLog.Fatal("Invalid GStop from $line!"), @@ -136,10 +135,10 @@ data class GStop( locationType = line[LOCATION_TYPE]?.takeIf { it.isNotBlank() }?.toInt(), parentStationId = line[PARENT_STATION]?.takeIf { it.isNotBlank() }?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it }, - stopTimezone = line[STOP_TIMEZONE] + stopTimezone = line[STOP_TIMEZONE]?.trim() ?.takeIf { it.isNotBlank() } ?.also { gStopTimezone -> - if (!ZoneId.getAvailableZoneIds().contains(gStopTimezone)) { + if (!availableZoneIds.contains(gStopTimezone)) { throw MTLog.Fatal("Invalid stop timezone in $line!") } } From e3143841a7096ac0feccfda7ec8cc7a0a6ba25ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 18 Aug 2026 15:10:59 -0400 Subject: [PATCH 7/7] merge stop TZ --- src/main/java/org/mtransit/parser/gtfs/GReader.java | 3 ++- src/main/java/org/mtransit/parser/gtfs/data/GStop.kt | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index 98644065..32b05549 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -582,8 +582,9 @@ private static void processStop(GAgencyTools agencyTools, GSpec gSpec, Map