diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index 13f47c1..32b0554 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; @@ -175,8 +176,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 @@ -412,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; @@ -546,14 +552,15 @@ 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, AVAILABLE_TIME_ZONE_IDS) + : GStop.fromLine(line, agencyTimezone, AVAILABLE_TIME_ZONE_IDS, agencyTools); if (agencyTools.excludeStop(gStop)) { //noinspection DiscouragedApi logExclude("Exclude stop: %s.", line.get(GStop.STOP_ID)); @@ -575,8 +582,9 @@ 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] ?: throw MTLog.Fatal("Invalid GAgency from $line!"), + agencyTimezone = line[AGENCY_TIMEZONE]?.trim() + ?.takeIf { availableZoneIds.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 98e2eaf..6d4429c 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,16 +95,19 @@ data class GStop( stopUrl = null, // TODO locationType = locationType.id, parentStationId = _parentStationId, + stopTimezone = stopTimezone, wheelchairBoarding = wheelchairBoarding.id, ) fun clone( stopLat: Double, stopLong: Double, + stopTimezone: String?, wheelchairBoarding: GWheelchairBoardingType, ) = this.copy( stopLat = stopLat, stopLong = stopLong, + stopTimezone = stopTimezone, wheelchairBoarding = wheelchairBoarding, ) @@ -115,11 +121,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?, availableZoneIds: Set, agencyTools: GAgencyTools? = null) = GStop( stopId = line[STOP_ID]?.trim() ?.let { agencyTools?.cleanStopOriginalId(it) ?: it } ?: throw MTLog.Fatal("Invalid GStop from $line!"), @@ -130,6 +137,14 @@ 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]?.trim() + ?.takeIf { it.isNotBlank() } + ?.also { gStopTimezone -> + if (!availableZoneIds.contains(gStopTimezone)) { + throw MTLog.Fatal("Invalid stop timezone in $line!") + } + } + ?.takeIf { it != agencyTimezone }, wheelchairBoarding = line[WHEELCHAIR_BOARDING]?.takeIf { it.isNotBlank() }?.toInt(), ) @@ -146,6 +161,7 @@ data class GStop( stopCode = it.stopCode ?: EMPTY, locationType = it.locationType, parentStationId = it.parentStationId, + stopTimezone = it.stopTimezone, wheelchairBoarding = it.wheelchairBoarding, ) } @@ -154,5 +170,11 @@ data class GStop( fun mergeLocation(loc1: Double, loc2: Double): Double { return floor((loc1 + loc2) / 2.00) } + + @JvmStatic + fun mergeTimezone(tz1: String?, tz2: String?): String? { + if (tz1 == tz2) return tz1 // only kept if same + return null // will use agency TZ + } } -} \ No newline at end of file +} diff --git a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java index 112dc1f..c13dc1f 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 )); } 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 5ed630e..3b58db1 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 @@ -15,6 +16,7 @@ data class MStop( val lng: Double, val accessible: Int, private val originalIdHash: Int, + val timeZoneId: String?, ) : Comparable { constructor( @@ -25,6 +27,7 @@ data class MStop( lng: Double, accessible: Int, originalId: String, + timeZoneId: String?, agencyTools: GAgencyTools? = null, ) : this( id, @@ -34,21 +37,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.orEmpty().quotesEscapeId()) // time zone ID (can contain "_") + } + }.joinToString(SQLUtils.COLUMN_SEPARATOR) override fun compareTo(other: MStop): Int { return id - other.id