From c618a2865dd314dd9f9de77eb4724027aae55911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Tue, 7 Jul 2026 21:11:27 -0400 Subject: [PATCH 01/10] Use GTFS directions file when provided #lio --- .../java/org/mtransit/parser/FileUtils.java | 9 ++-- .../org/mtransit/parser/gtfs/GReader.java | 42 +++++++++++++++--- .../mtransit/parser/gtfs/data/GDirection.kt | 14 ++++-- .../org/mtransit/parser/gtfs/data/GSpec.java | 44 +++++++++---------- .../parser/mt/MDirectionHeadSignFinder.kt | 20 +++++++-- 5 files changed, 90 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/mtransit/parser/FileUtils.java b/src/main/java/org/mtransit/parser/FileUtils.java index 1fd8726a..35f9ea3d 100644 --- a/src/main/java/org/mtransit/parser/FileUtils.java +++ b/src/main/java/org/mtransit/parser/FileUtils.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.List; @SuppressWarnings("unused") public final class FileUtils { @@ -55,7 +56,7 @@ public static String sizeToDiplayString(@Nullable Long size) { } @Nullable - public static File findFileCaseInsensitive(@NotNull String directoryPath, @NotNull String fileNameToFind) { + public static File findFileCaseInsensitive(@NotNull String directoryPath, @NotNull List fileNamesToFind) { final File directory = new File(directoryPath); // Check if the provided path is a directory and exists if (!directory.isDirectory()) { @@ -66,8 +67,10 @@ public static File findFileCaseInsensitive(@NotNull String directoryPath, @NotNu if (files != null) { for (File file : files) { if (file.isFile()) { // Check if it's a file (not a directory) - if (file.getName().equalsIgnoreCase(fileNameToFind)) { - return file; // Found the file (case-insensitive) + for (String fileNameToFind : fileNamesToFind) { + if (file.getName().equalsIgnoreCase(fileNameToFind)) { + return file; // Found the file (case-insensitive) + } } } } diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index 69070580..d3c06346 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -36,6 +36,7 @@ import java.sql.PreparedStatement; import java.text.DateFormat; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -125,7 +126,7 @@ public static GSpec readGtfsZipFile( } // DIRECTIONS (ext) (after route) if (!calendarsOnly && !routeTripCalendarsOnly) { - readFile(gtfsDir, GDirection.FILENAME, false, line -> + readFiles(gtfsDir, GDirection.getFILENAMES(), false, line -> processDirection(agencyTools, gSpec, line, skipDataCleanup) ); } @@ -183,9 +184,24 @@ private static boolean readFile( boolean fileRequired, @NotNull LineProcessor lineProcessor ) { - return readFile(gtfsDir, fileName, fileRequired, lineProcessor, null); + return readFiles( + gtfsDir, + Collections.singletonList(fileName), + fileRequired, + lineProcessor + ); } + private static boolean readFiles( + @NotNull String gtfsDir, + @NotNull List fileNames, + boolean fileRequired, + @NotNull LineProcessor lineProcessor + ) { + return readFiles(gtfsDir, fileNames, fileRequired, lineProcessor, null); + } + + @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"}) private static boolean readFile( @NotNull String gtfsDir, @NotNull String fileName, @@ -193,12 +209,28 @@ private static boolean readFile( @NotNull LineProcessor lineProcessor, @Nullable OnColumnNamesFound onColumnNamesFoundCallback ) { - final File gtfsFile = FileUtils.findFileCaseInsensitive(gtfsDir, fileName); + return readFiles( + gtfsDir, + Collections.singletonList(fileName), + fileRequired, + lineProcessor, + onColumnNamesFoundCallback + ); + } + + private static boolean readFiles( + @NotNull String gtfsDir, + @NotNull List fileNames, + boolean fileRequired, + @NotNull LineProcessor lineProcessor, + @Nullable OnColumnNamesFound onColumnNamesFoundCallback + ) { + final File gtfsFile = FileUtils.findFileCaseInsensitive(gtfsDir, fileNames); if (gtfsFile == null || !gtfsFile.exists()) { if (fileRequired) { throw new MTLog.Fatal("'%s' file does not exist!", gtfsFile); } else { - MTLog.log("Reading file '%s'... SKIP (non-existing).", fileName); + MTLog.log("Reading file(s) '%s'... SKIP (non-existing).", fileNames); return false; } } @@ -443,7 +475,7 @@ private static void processDirection(GAgencyTools agencyTools, GSpec gSpec, Hash logExclude("Exclude direction (!route): %s | %s.", gRoute == null ? null : gRoute.getRouteId(), gDirection.getDirectionId()); return; } - final GDirection existingDirection = gSpec.getDirection(gDirection.getRouteIdInt(), gDirection.getDirectionId()); + final GDirection existingDirection = gSpec.getRouteDirection(gDirection.getRouteIdInt(), gDirection.getDirectionId().getId()); if (existingDirection != null) { //noinspection DiscouragedApi MTLog.logDebug("Duplicate direction ID for route ID! (new:%s|old:%s)", gDirection.getDirectionId(), existingDirection.getDirectionId()); diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt index c34dce8b..e50bed4d 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt @@ -1,7 +1,6 @@ package org.mtransit.parser.gtfs.data import androidx.annotation.Discouraged -import org.mtransit.commons.GTFSCommons import org.mtransit.commons.gtfs.data.Direction import org.mtransit.commons.gtfs.data.DirectionType import org.mtransit.parser.MTLog @@ -46,7 +45,12 @@ data class GDirection( ) companion object { - const val FILENAME = "directions.txt" + + @JvmStatic + val FILENAMES = listOf( + "directions.txt", + "route_directions.txt" + ) private const val ROUTE_ID = GRoute.ROUTE_ID private const val DIRECTION_ID = "direction_id" @@ -54,6 +58,7 @@ data class GDirection( private const val DESTINATION = "destination" private const val DESTINATION_DIRECTION_NAME = "direction_name" + private const val DESTINATION_ROUTE_DIRECTION_NAME = "route_direction_name" // TODO other alternatives @JvmOverloads @@ -64,7 +69,10 @@ data class GDirection( ?: throw MTLog.Fatal("Invalid GDirection from $line!"), directionIdInt = line[DIRECTION_ID]?.toIntOrNull() ?: throw MTLog.Fatal("Invalid GDirection from $line!"), directionTypeValue = line[DIRECTION]?.trim(), - destination = line[DESTINATION]?.trim() ?: line[DESTINATION_DIRECTION_NAME]?.trim(), + destination = line[DESTINATION]?.trim() + ?: line[DESTINATION_DIRECTION_NAME]?.trim() + ?: line[DESTINATION_ROUTE_DIRECTION_NAME]?.trim() + , ) @JvmStatic diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java b/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java index 067d1c42..2f617b92 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java +++ b/src/main/java/org/mtransit/parser/gtfs/data/GSpec.java @@ -100,7 +100,7 @@ public GAgency getSingleAgency() { } @Nullable - public GAgency getAgency(@NotNull Integer agencyIdInt) { + public GAgency getAgency(int agencyIdInt) { if (USE_DB_ONLY) { return GAgency.from(GTFSDataBase.selectAgency(GIDs.getString(agencyIdInt))); } @@ -259,7 +259,7 @@ public List getRoutes(@Nullable Long optMRouteId) { } @Nullable - public GRoute getRoute(@NotNull Integer routeIdInt) { + public GRoute getRoute(int routeIdInt) { if (USE_DB_ONLY) { return GRoute.from(GTFSDataBase.selectRoute(GIDs.getString(routeIdInt))); } @@ -311,7 +311,7 @@ public GStop getStop(@NotNull String gStopId) { } @Nullable - public GStop getStop(@NotNull Integer gStopIdInt) { + public GStop getStop(int gStopIdInt) { if (USE_DB_ONLY) { return GStop.from(GTFSDataBase.selectStop(GIDs.getString(gStopIdInt))); } @@ -389,7 +389,7 @@ public void updateTripDirectionId(int directionId, @Nullable Collection } @Nullable - public GTrip getTrip(@NotNull Integer tripIdInt) { + public GTrip getTrip(int tripIdInt) { if (USE_DB_ONLY) { return GTrip.from(GTFSDataBase.selectTrip(GIDs.getString(tripIdInt))); } @@ -410,7 +410,7 @@ private Integer getTripRouteId(Integer tripIdInt) { return this.tripIdIntRouteIdInt.get(tripIdInt); } - public boolean hasTripsOriginalRouteId(@NotNull Integer routeIdInt) { + public boolean hasTripsOriginalRouteId(int routeIdInt) { return this.tripOriginalRouteIdInt.contains(routeIdInt); } @@ -424,7 +424,7 @@ public List getTrips(@Nullable Integer optRouteId) { } @NotNull - public List getRouteTrips(@NotNull Integer routeIdInt) { + public List getRouteTrips(int routeIdInt) { if (USE_DB_ONLY) { return GTrip.from(GTFSDataBase.selectTrips(null, Collections.singleton(GIDs.getString(routeIdInt)))); } @@ -437,7 +437,7 @@ public void addDirection(@NotNull GDirection gDirection) { } @Nullable - public List getDirection(@NotNull Integer routeIdInt) { + public List getRouteDirections(int routeIdInt) { if (USE_DB_ONLY) { return GDirection.from(GTFSDataBase.selectDirections(GIDs.getString(routeIdInt))); } @@ -445,11 +445,11 @@ public List getDirection(@NotNull Integer routeIdInt) { } @Nullable - public GDirection getDirection(@NotNull Integer routeIdInt, @NotNull GDirectionId directionId) { - List directions = getDirection(routeIdInt); + public GDirection getRouteDirection(int routeIdInt, int directionId) { + final List directions = getRouteDirections(routeIdInt); if (directions != null) { for (GDirection direction : directions) { - if (direction.getDirectionId() == directionId) { + if (direction.getDirectionId().getId() == directionId) { return direction; } } @@ -466,14 +466,13 @@ public void remove(long mRouteId) { } @NotNull - public List getStopTimes(@NotNull Long mRouteId, - @NotNull Integer gTripIdInt) { + public List getStopTimes(long mRouteId, int gTripIdInt) { return getStopTimes(mRouteId, gTripIdInt, null, null); } @NotNull - public List getStopTimes(@NotNull Long mRouteId, - @NotNull Integer gTripIdInt, + public List getStopTimes(long mRouteId, + int gTripIdInt, @SuppressWarnings("unused") @Nullable String optGStopId, @SuppressWarnings("unused") @Nullable Integer optGStopSequence) { GenerateMObjectsTask routeGenerator = this.routeGenerators.get(mRouteId); @@ -491,7 +490,7 @@ public void addStopTime(@NotNull GStopTime gStopTime, @NotNull PreparedStatement GTFSDataBase.insertStopTime(gStopTime.to(), insertStopTimePrepared); } - private int removeTripStopTimes(@NotNull Integer gTripId) { + private int removeTripStopTimes(int gTripId) { int r = 0; r += GTFSDataBase.deleteStopTimes(GIDs.getString(gTripId)); return r; @@ -739,9 +738,6 @@ public void generateStopTimesFromFrequencies(@SuppressWarnings("unused") @NotNul for (GStopTime gStopTime : tripStopTimes) { stopTimeCal.add(Calendar.SECOND, gStopTimeIncInSec.get(gStopTime.getUID())); final int newDepartureTime = getNewDepartureTime(stopTimeCal); - final GPickupType pickupType = gStopTime.getPickupType(); - final GDropOffType dropOffType = gStopTime.getDropOffType(); - final GTimePoint timePoint = gStopTime.getTimePoint(); final GStopTime newGStopTime = new GStopTime( newGeneratedTripIdInt, newDepartureTime, @@ -749,9 +745,9 @@ public void generateStopTimesFromFrequencies(@SuppressWarnings("unused") @NotNul gStopTime.getStopIdInt(), gStopTime.getStopSequence(), gStopTime.getStopHeadsign(), - pickupType, - dropOffType, - timePoint + gStopTime.getPickupType(), + gStopTime.getDropOffType(), + gStopTime.getTimePoint() ); newGStopTimes.add(newGStopTime); } @@ -832,7 +828,7 @@ public Set getRouteIds() { } @NotNull - public GSpec getRouteGTFS(@SuppressWarnings("unused") @NotNull Long mRouteId) { + public GSpec getRouteGTFS(@SuppressWarnings("unused") long mRouteId) { return this; } @@ -972,7 +968,7 @@ public void clearRawData() { } @SuppressWarnings("unused") - public void cleanupRouteGTFS(@NotNull Long mRouteId) { + public void cleanupRouteGTFS(long mRouteId) { MTLog.log("%d: Removing route data...", mRouteId); int r = 0; try { @@ -989,7 +985,7 @@ public void cleanupRouteGTFS(@NotNull Long mRouteId) { MTLog.log("%d: Removing route data...DONE (%d removed objects)", mRouteId, r); } - public boolean hasRouteTrips(@NotNull Long mRouteId) { + public boolean hasRouteTrips(long mRouteId) { return this.mRouteWithTripIds.contains(mRouteId); } diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index d4bfdc07..2d6ceb95 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -1,5 +1,6 @@ package org.mtransit.parser.mt +import org.jetbrains.annotations.VisibleForTesting import org.mtransit.commons.StringUtils.EMPTY import org.mtransit.commons.containsExactList import org.mtransit.commons.indexOf @@ -40,6 +41,16 @@ object MDirectionHeadSignFinder { val directionRouteIdInts = mutableMapOf>() GDirectionId.entries.forEach { gDirectionId -> val directionId = gDirectionId.id + gRouteTrips.map { it.routeIdInt }.distinct().singleOrNull()?.let { routeIdInt -> + val routeDirection = routeGTFS.getRouteDirection(routeIdInt, directionId) + routeDirection?.destination + ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, true, it) } + ?.takeIf { it.isNotBlank() } + ?.let { + directionHeadSigns[directionId] = it + return@forEach + } + } findDirectionHeadSign( routeId, gRouteTrips, @@ -85,7 +96,8 @@ object MDirectionHeadSignFinder { ?: continue val rln = gRoute.routeLongNameOrDefault.takeIf { it.isNotBlank() } ?: continue - routeDirectionHeadSigns[directionId] = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, agencyTools.cleanRouteLongName(rln)) + routeDirectionHeadSigns[directionId] = + agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, agencyTools.cleanRouteLongName(rln)) } if (routeDirectionHeadSigns.size == directionHeadSigns.size // all route long name or nothing && agencyTools.directionHeadSignsDescriptive(routeDirectionHeadSigns) @@ -174,7 +186,8 @@ object MDirectionHeadSignFinder { return directionHeadSigns } - fun findDirectionHeadSign( + @VisibleForTesting + internal fun findDirectionHeadSign( routeId: Long, gRouteTrips: List, routeGTFS: GSpec, @@ -186,9 +199,8 @@ object MDirectionHeadSignFinder { gTrip.directionIdOrDefault == directionId }.map { gTrip -> val routeIdInt = gTrip.routeIdInt - val gRoute = routeGTFS.getRoute(routeIdInt) val headSign = gTrip.tripHeadsign - ?.let { agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, true, it) } ?: EMPTY + ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, true, it) } ?: EMPTY val stopTimes = routeGTFS.getStopTimes(routeId, gTrip.tripIdInt, null, null) Triple(routeIdInt, headSign, stopTimes) }.filterNot { (_, _, stopTimes) -> From e97f5b8dfe276df3adfd79bfd0123062a7487bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 08:38:29 -0400 Subject: [PATCH 02/10] STM compat --- .../org/mtransit/parser/gtfs/data/GDirection.kt | 10 ++++++---- .../parser/mt/MDirectionHeadSignFinder.kt | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt index e50bed4d..dd741a54 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt @@ -57,8 +57,9 @@ data class GDirection( private const val DIRECTION = "direction" private const val DESTINATION = "destination" - private const val DESTINATION_DIRECTION_NAME = "direction_name" - private const val DESTINATION_ROUTE_DIRECTION_NAME = "route_direction_name" + private const val DIRECTION_NAME = "direction_name" + private const val ROUTE_DIRECTION_NAME = "route_direction_name" + private const val DIRECTION_LEGACY = "direction_legacy" // #STM // TODO other alternatives @JvmOverloads @@ -70,8 +71,9 @@ data class GDirection( directionIdInt = line[DIRECTION_ID]?.toIntOrNull() ?: throw MTLog.Fatal("Invalid GDirection from $line!"), directionTypeValue = line[DIRECTION]?.trim(), destination = line[DESTINATION]?.trim() - ?: line[DESTINATION_DIRECTION_NAME]?.trim() - ?: line[DESTINATION_ROUTE_DIRECTION_NAME]?.trim() + ?: line[DIRECTION_NAME]?.trim() + ?: line[ROUTE_DIRECTION_NAME]?.trim() + ?: line[DIRECTION_LEGACY]?.trim() , ) diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index 2d6ceb95..53c9328b 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -41,15 +41,25 @@ object MDirectionHeadSignFinder { val directionRouteIdInts = mutableMapOf>() GDirectionId.entries.forEach { gDirectionId -> val directionId = gDirectionId.id - gRouteTrips.map { it.routeIdInt }.distinct().singleOrNull()?.let { routeIdInt -> + val directionTrips = gRouteTrips.filter { it.directionId == directionId } + if (directionTrips.isEmpty()) { + MTLog.logDebug("$routeId: $directionId: no trips -> no head-sign.") + return@forEach + } + val directionRoutesIdInts = directionTrips.map { it.routeIdInt }.distinct() + MTLog.logDebug("$routeId: $directionId: direction original route ID(s): ${GIDs.toStringPlus(directionRoutesIdInts)} (trips: ${directionTrips.size})") + directionRoutesIdInts.singleOrNull()?.let { routeIdInt -> val routeDirection = routeGTFS.getRouteDirection(routeIdInt, directionId) - routeDirection?.destination - ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, true, it) } + ?: return@let + routeDirection.destination + ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, false, it) } ?.takeIf { it.isNotBlank() } ?.let { + MTLog.logDebug("$routeId: $directionId: direction > use '$it'.") directionHeadSigns[directionId] = it return@forEach } + MTLog.logDebug("$routeId: $directionId: ignore direction ${routeDirection.toStringPlus()}'.") } findDirectionHeadSign( routeId, From a1ca28924dd5472ead3535bf2e2fa6ba1bc0a6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 09:44:38 -0400 Subject: [PATCH 03/10] compat w/ STM bus (invalid values in directions.txt) --- .../mtransit/parser/DefaultAgencyTools.java | 36 +++++++++++++------ .../parser/config/gtfs/data/RouteConfig.kt | 2 ++ .../mtransit/parser/gtfs/GAgencyTools.java | 4 +++ .../parser/mt/MDirectionHeadSignFinder.kt | 14 ++++---- .../parser/mt/MDirectionHeadSignFinderTest.kt | 6 ++-- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index 36438fb5..5273279c 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -1,5 +1,7 @@ package org.mtransit.parser; +import static org.mtransit.parser.Constants.EMPTY; + import androidx.annotation.Discouraged; import androidx.annotation.VisibleForTesting; @@ -871,9 +873,10 @@ public void setDirectionHeadsign(@NotNull MRoute mRoute, @NotNull MDirection mDi } final boolean fromStopName = mDirection.getHeadsignType() == MDirection.HEADSIGN_TYPE_STOP_ID; final boolean fromTripHeadSign = mDirection.getHeadsignType() == MDirection.HEADSIGN_TYPE_STRING; + final boolean fromDirection = false; // using trip head-sign here if (directionFinderEnabled(mRoute.getId(), gRoute)) { mDirection.setHeadsignString( - cleanDirectionHeadsign(gRoute, gTrip.getDirectionIdOrDefault(), fromStopName, fromTripHeadSign, gTrip.getTripHeadsignOrDefault()), + cleanDirectionHeadsign(gRoute, gTrip.getDirectionIdOrDefault(), fromStopName, fromTripHeadSign, fromDirection, gTrip.getTripHeadsignOrDefault()), gTrip.getDirectionIdOrDefault() ); return; @@ -883,7 +886,7 @@ public void setDirectionHeadsign(@NotNull MRoute mRoute, @NotNull MDirection mDi } try { mDirection.setHeadsignString( - cleanDirectionHeadsign(gRoute, gTrip.getDirectionIdOrDefault(), fromStopName, fromTripHeadSign, gTrip.getTripHeadsignOrDefault()), + cleanDirectionHeadsign(gRoute, gTrip.getDirectionIdOrDefault(), fromStopName, fromTripHeadSign, fromDirection, gTrip.getTripHeadsignOrDefault()), gTrip.getDirectionIdOrDefault() ); } catch (NumberFormatException nfe) { @@ -997,10 +1000,23 @@ public boolean removeRouteDescFromDirectionHeadsign() { * @param directionId {@link org.mtransit.parser.gtfs.data.GDirectionId} (0 or 1 or missing/generated) */ @Override + public @NotNull String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, boolean fromTripHeadSign, boolean fromDirection, @NotNull String directionHeadSign) { + if (fromDirection) { + if (Configs.getRouteConfig().getDirectionHeadsignIgnoreProvidedDirection()) { + return EMPTY; + } + } + //noinspection deprecation + return cleanDirectionHeadsign(gRoute, directionId, fromStopName, fromTripHeadSign, directionHeadSign); + } + + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @Override public @NotNull String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, boolean fromTripHeadSign, @NotNull String directionHeadSign) { if (fromTripHeadSign) { if (Configs.getRouteConfig().getDirectionHeadsignIgnoreTripHeadsign()) { - directionHeadSign = ""; + return EMPTY; } } //noinspection deprecation @@ -1013,16 +1029,15 @@ public boolean removeRouteDescFromDirectionHeadsign() { public @NotNull String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, @NotNull String directionHeadSign) { if (gRoute != null) { if (removeRouteLongNameFromDirectionHeadsign() && directionHeadSign.equals(gRoute.getRouteLongNameOrDefault())) { - directionHeadSign = ""; + return EMPTY; } else if (removeRouteShortNameFromDirectionHeadsign() && directionHeadSign.equals(gRoute.getRouteShortName())) { - directionHeadSign = ""; + return EMPTY; } else if (removeRouteDescFromDirectionHeadsign() && directionHeadSign.equals(gRoute.getRouteDescOrDefault())) { - directionHeadSign = ""; + return EMPTY; } } //noinspection deprecation - directionHeadSign = cleanDirectionHeadsign(directionId, fromStopName, directionHeadSign); - return directionHeadSign; + return cleanDirectionHeadsign(directionId, fromStopName, directionHeadSign); } @SuppressWarnings("DeprecatedIsStillUsed") @@ -1030,9 +1045,10 @@ public boolean removeRouteDescFromDirectionHeadsign() { @NotNull @Override public String cleanDirectionHeadsign(int directionId, boolean fromStopName, @NotNull String directionHeadSign) { - directionHeadSign = Configs.getRouteConfig().cleanDirectionHeadsign(getFirstLanguageNN(), directionHeadSign); //noinspection deprecation - return cleanDirectionHeadsign(fromStopName, directionHeadSign); + return cleanDirectionHeadsign(fromStopName, + Configs.getRouteConfig().cleanDirectionHeadsign(getFirstLanguageNN(), directionHeadSign) + ); } @SuppressWarnings("DeprecatedIsStillUsed") diff --git a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt index 3b5b2eb3..d5cea44a 100644 --- a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt +++ b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt @@ -92,6 +92,8 @@ data class RouteConfig( val directionHeadsignRemoveRouteShortName: Boolean = false, // OPT-IN feature @SerialName("direction_headsign_remove_route_desc") val directionHeadsignRemoveRouteDesc: Boolean = false, // OPT-IN feature + @SerialName("direction_headsign_ignore_provided_direction") + val directionHeadsignIgnoreProvidedDirection: Boolean = false, @SerialName("direction_headsign_ignore_trip_headsign") val directionHeadsignIgnoreTripHeadsign: Boolean = false, // OPT-IN feature @SerialName("direction_splitter_enabled") diff --git a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java index a45fb2d7..d24a5c9e 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java +++ b/src/main/java/org/mtransit/parser/gtfs/GAgencyTools.java @@ -214,6 +214,10 @@ public interface GAgencyTools { boolean removeRouteDescFromDirectionHeadsign(); + @NotNull + String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, boolean fromTripHeadSign, boolean fromDirection, @NotNull String directionHeadSign); + + @Deprecated @NotNull String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, boolean fromTripHeadSign, @NotNull String directionHeadSign); diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index 53c9328b..0eb64b77 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -52,7 +52,7 @@ object MDirectionHeadSignFinder { val routeDirection = routeGTFS.getRouteDirection(routeIdInt, directionId) ?: return@let routeDirection.destination - ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, false, it) } + ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, false, true, it) } ?.takeIf { it.isNotBlank() } ?.let { MTLog.logDebug("$routeId: $directionId: direction > use '$it'.") @@ -85,9 +85,9 @@ object MDirectionHeadSignFinder { val firstAndLastTime = directionAmPm[directionId] ?: continue val gRoute = directionRouteIdInts[directionId].takeIf { it?.size == 1 }?.getOrNull(0)?.let { routeGTFS.getRoute(it) } if (GTime.areAM(firstAndLastTime)) { - amPmDirectionHeadSigns[directionId] = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, "AM") + amPmDirectionHeadSigns[directionId] = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, false, "AM") } else if (GTime.arePM(firstAndLastTime)) { - amPmDirectionHeadSigns[directionId] = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, "PM") + amPmDirectionHeadSigns[directionId] = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, false, "PM") } } if (amPmDirectionHeadSigns.size == 2 // all AM/PM or nothing @@ -107,7 +107,7 @@ object MDirectionHeadSignFinder { val rln = gRoute.routeLongNameOrDefault.takeIf { it.isNotBlank() } ?: continue routeDirectionHeadSigns[directionId] = - agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, agencyTools.cleanRouteLongName(rln)) + agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, false, agencyTools.cleanRouteLongName(rln)) } if (routeDirectionHeadSigns.size == directionHeadSigns.size // all route long name or nothing && agencyTools.directionHeadSignsDescriptive(routeDirectionHeadSigns) @@ -125,7 +125,7 @@ object MDirectionHeadSignFinder { val stop = routeGTFS.getStop(stopIdInt) ?: continue val gRoute = directionRouteIdInts[directionId].takeIf { it?.size == 1 }?.getOrNull(0)?.let { routeGTFS.getRoute(it) } lastStopDirectionHeadSigns[directionId] = - agencyTools.cleanDirectionHeadsign(gRoute, directionId, true, false, agencyTools.cleanStopName(stop.stopName)) + agencyTools.cleanDirectionHeadsign(gRoute, directionId, true, false, false, agencyTools.cleanStopName(stop.stopName)) MTLog.logDebug("$routeId: $directionId Stop '${stop.toStringPlus(false)}' > '${lastStopDirectionHeadSigns[directionId]}'.") } val allDirectionHeadSignsEmpty: Boolean = directionHeadSigns @@ -159,7 +159,7 @@ object MDirectionHeadSignFinder { val gRoute = directionRouteIdInts[directionId].takeIf { it?.size == 1 }?.getOrNull(0)?.let { routeGTFS.getRoute(it) } var cleanDirectionHeadsign: String? = null for (stopTimeHeadSign in stopTimeHeadSigns) { - cleanDirectionHeadsign = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, stopTimeHeadSign) // already cleaned + cleanDirectionHeadsign = agencyTools.cleanDirectionHeadsign(gRoute, directionId, false, false, false, stopTimeHeadSign) // already cleaned if (cleanDirectionHeadsign.isNotBlank()) { break } @@ -210,7 +210,7 @@ object MDirectionHeadSignFinder { }.map { gTrip -> val routeIdInt = gTrip.routeIdInt val headSign = gTrip.tripHeadsign - ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, true, it) } ?: EMPTY + ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, true, false, it) } ?: EMPTY val stopTimes = routeGTFS.getStopTimes(routeId, gTrip.tripIdInt, null, null) Triple(routeIdInt, headSign, stopTimes) }.filterNot { (_, _, stopTimes) -> diff --git a/src/test/java/org/mtransit/parser/mt/MDirectionHeadSignFinderTest.kt b/src/test/java/org/mtransit/parser/mt/MDirectionHeadSignFinderTest.kt index ac7ae4bf..32555a4e 100644 --- a/src/test/java/org/mtransit/parser/mt/MDirectionHeadSignFinderTest.kt +++ b/src/test/java/org/mtransit/parser/mt/MDirectionHeadSignFinderTest.kt @@ -42,9 +42,9 @@ class MDirectionHeadSignFinderTest { @Suppress("DEPRECATION") @Before fun setUp() { - whenever(agencyTools.cleanDirectionHeadsign(anyOrNull(), anyInt(), anyBoolean(), anyBoolean(), anyString())) + whenever(agencyTools.cleanDirectionHeadsign(anyOrNull(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), anyString())) .then { - it.arguments[4] + it.arguments[5] } whenever(agencyTools.cleanStopHeadSign(any(), any(), any(), anyString())) .then { @@ -821,6 +821,7 @@ class MDirectionHeadSignFinderTest { assertEquals(false, result?.firstAndLast?.let { GTime.arePM(it) } == true) } + @Suppress("SameParameterValue") private fun makeGTrip( routeId: String, serviceId: String, @@ -843,6 +844,7 @@ class MDirectionHeadSignFinderTest { bikesAllowed = null, ) + @Suppress("SameParameterValue") private fun makeGTrip( routeId: Int, serviceId: Int, From 66b6c01d9d8546c0fd14316427093287324784ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 10:01:58 -0400 Subject: [PATCH 04/10] PR comments --- src/main/java/org/mtransit/parser/FileUtils.java | 10 ++++------ src/main/java/org/mtransit/parser/gtfs/GReader.java | 2 +- .../org/mtransit/parser/mt/MDirectionHeadSignFinder.kt | 9 ++++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/mtransit/parser/FileUtils.java b/src/main/java/org/mtransit/parser/FileUtils.java index 35f9ea3d..dc4efdca 100644 --- a/src/main/java/org/mtransit/parser/FileUtils.java +++ b/src/main/java/org/mtransit/parser/FileUtils.java @@ -65,12 +65,10 @@ public static File findFileCaseInsensitive(@NotNull String directoryPath, @NotNu } final File[] files = directory.listFiles(); // Get all files and subdirectories if (files != null) { - for (File file : files) { - if (file.isFile()) { // Check if it's a file (not a directory) - for (String fileNameToFind : fileNamesToFind) { - if (file.getName().equalsIgnoreCase(fileNameToFind)) { - return file; // Found the file (case-insensitive) - } + for (String fileNameToFind : fileNamesToFind) { + for (File file : files) { + if (file.isFile() && file.getName().equalsIgnoreCase(fileNameToFind)) { + return file; // Found the file (case-insensitive) } } } diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index d3c06346..ca0a0b5e 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -228,7 +228,7 @@ private static boolean readFiles( final File gtfsFile = FileUtils.findFileCaseInsensitive(gtfsDir, fileNames); if (gtfsFile == null || !gtfsFile.exists()) { if (fileRequired) { - throw new MTLog.Fatal("'%s' file does not exist!", gtfsFile); + throw new MTLog.Fatal("'%s' file does not exist!", fileNames); } else { MTLog.log("Reading file(s) '%s'... SKIP (non-existing).", fileNames); return false; diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index 0eb64b77..6f91e324 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -46,9 +46,12 @@ object MDirectionHeadSignFinder { MTLog.logDebug("$routeId: $directionId: no trips -> no head-sign.") return@forEach } - val directionRoutesIdInts = directionTrips.map { it.routeIdInt }.distinct() - MTLog.logDebug("$routeId: $directionId: direction original route ID(s): ${GIDs.toStringPlus(directionRoutesIdInts)} (trips: ${directionTrips.size})") - directionRoutesIdInts.singleOrNull()?.let { routeIdInt -> + val directionRouteIdInt = gRouteTrips.firstOrNull()?.routeIdInt + ?.takeIf { firstTripRouteIdInt -> + gRouteTrips.all { it.routeIdInt == firstTripRouteIdInt } + } + MTLog.logDebug("$routeId: $directionId: direction original route ID(s): ${GIDs.toStringPlus(directionRouteIdInt)} (trips: ${directionTrips.size})") + directionRouteIdInt?.let { routeIdInt -> val routeDirection = routeGTFS.getRouteDirection(routeIdInt, directionId) ?: return@let routeDirection.destination From b0049fe48215d3290c2bd04e45689e9766e5be8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 13:03:10 -0400 Subject: [PATCH 05/10] + `direction_headsign_ignore_provided_direction_until` #STMbus --- .../java/org/mtransit/parser/DefaultAgencyTools.java | 3 ++- .../mtransit/parser/config/gtfs/data/RouteConfig.kt | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index 5273279c..ff4f6b02 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -1002,7 +1002,8 @@ public boolean removeRouteDescFromDirectionHeadsign() { @Override public @NotNull String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, boolean fromTripHeadSign, boolean fromDirection, @NotNull String directionHeadSign) { if (fromDirection) { - if (Configs.getRouteConfig().getDirectionHeadsignIgnoreProvidedDirection()) { + //noinspection DiscouragedApi + if (gRoute != null && Configs.getRouteConfig().isDirectionHeadsignIgnoreProvidedDirection(getTodayDateInt(), gRoute.getOriginalRouteId())) { return EMPTY; } } diff --git a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt index d5cea44a..edc28000 100644 --- a/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt +++ b/src/main/java/org/mtransit/parser/config/gtfs/data/RouteConfig.kt @@ -93,7 +93,9 @@ data class RouteConfig( @SerialName("direction_headsign_remove_route_desc") val directionHeadsignRemoveRouteDesc: Boolean = false, // OPT-IN feature @SerialName("direction_headsign_ignore_provided_direction") - val directionHeadsignIgnoreProvidedDirection: Boolean = false, + val directionHeadsignIgnoreProvidedDirection: Boolean = false, // OPT-IN feature + @SerialName("direction_headsign_ignore_provided_direction_until") + val directionHeadsignIgnoreProvidedDirectionUntil: Map = emptyMap(), // OPT-IN feature @SerialName("direction_headsign_ignore_trip_headsign") val directionHeadsignIgnoreTripHeadsign: Boolean = false, // OPT-IN feature @SerialName("direction_splitter_enabled") @@ -455,6 +457,10 @@ data class RouteConfig( return string } + @JvmOverloads + fun isDirectionHeadsignIgnoreProvidedDirection(todayDate: Int, gOriginalRouteId: String? = null) = + directionHeadsignIgnoreProvidedDirection || isAllowedUntil(directionHeadsignIgnoreProvidedDirectionUntil[gOriginalRouteId], todayDate) + @JvmOverloads fun isDirectionSplitterEnabled(todayDate: Int, routeId: Long? = null) = directionSplitterEnabled && !isAllowedUntil(directionSplitterDisabledUntil[routeId], todayDate) @@ -472,6 +478,6 @@ data class RouteConfig( fun allowNonDescriptiveHeadSigns(todayDate: Int, mRouteId: Long) = allowNonDescriptiveHeadSigns[mRouteId] == true || isAllowedUntil(allowNonDescriptiveHeadSignsUntil[mRouteId], todayDate) - private fun isAllowedUntil(dateStr: String?, todayDate: Int) = + fun isAllowedUntil(dateStr: String?, todayDate: Int) = dateStr?.toIntOrNull()?.let { todayDate <= it } ?: false } From ea80f5defceb3cb3efb277aff668269a694d1475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 13:31:00 -0400 Subject: [PATCH 06/10] compat w/ invalid column counts in GTFS files and use destination OR direction type #TransLink --- .../org/mtransit/parser/gtfs/GReader.java | 19 ++++++++----------- .../mtransit/parser/gtfs/data/GDirection.kt | 7 +++++-- .../parser/mt/MDirectionHeadSignFinder.kt | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index ca0a0b5e..38a19a24 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -268,9 +268,7 @@ private static void readCsv( String line; String[] columnNames; line = reader.readLine(); - if (line == null || line.isEmpty()) { - return; - } + if (line == null || line.isEmpty()) return; if (line.charAt(0) == '\uFEFF') { // remove 1st empty char MTLog.log("Reading file '%s'... > remove 1st empty car", filename); line = String.copyValueOf(line.toCharArray(), 1, line.length() - 1); @@ -283,9 +281,7 @@ private static void readCsv( if (onColumnNamesFoundCallback != null) { onColumnNamesFoundCallback.processColumnNames(Arrays.asList(columnNames)); } - if (columnNames.length == 0) { - return; - } + if (columnNames.length == 0) return; List records; HashMap map = new HashMap<>(); String[] lineColumns = new String[columnNames.length]; @@ -306,10 +302,10 @@ private static void readCsv( } recordColumns = records.get(0); recordColumnsSize = recordColumns.size(); - if (columnNames.length != recordColumnsSize - && columnNames.length != (recordColumnsSize + 1)) { - MTLog.log("File '%s' line invalid: %s columns instead of %s: %s", filename, recordColumnsSize, columnNames.length, line); - continue; + if (recordColumnsSize > columnNames.length) { + MTLog.log("File '%s' line contains MORE columns (%s:%s) than expected (%s:%s)!", filename, recordColumnsSize, line, columnNames.length, Arrays.asList(columnNames)); + } else if (recordColumnsSize < columnNames.length) { + MTLog.log("File '%s' line contains LESS columns (%s:%s) than expected (%s:%s)!", filename, recordColumnsSize, line, columnNames.length, Arrays.asList(columnNames)); } for (int i = 0; i < lineColumns.length; i++) { final String lineColumn = i >= recordColumns.size() ? EMPTY : recordColumns.get(i); @@ -318,7 +314,8 @@ private static void readCsv( QUOTE_.matcher(lineColumn).replaceAll(EMPTY); } map.clear(); - for (int ci = 0; ci < recordColumnsSize; ++ci) { + final int usedColumnsCounts = Math.min(recordColumnsSize, columnNames.length); + for (int ci = 0; ci < usedColumnsCounts; ++ci) { map.put(columnNames[ci], lineColumns[ci]); } if (lineProcessor != null) { diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt index dd741a54..24165b6a 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt @@ -31,6 +31,10 @@ data class GDirection( private val _routeId: String get() = GIDs.getString(routeIdInt) + val destinationOrDirection: String? = + destination + ?: directionType.takeIf { it != GDirectionType.UNKNOWN }?.value + @Suppress("unused") fun toStringPlus(): String { return toString() + @@ -73,8 +77,7 @@ data class GDirection( destination = line[DESTINATION]?.trim() ?: line[DIRECTION_NAME]?.trim() ?: line[ROUTE_DIRECTION_NAME]?.trim() - ?: line[DIRECTION_LEGACY]?.trim() - , + ?: line[DIRECTION_LEGACY]?.trim(), ) @JvmStatic diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index 6f91e324..2d860c6a 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -54,7 +54,7 @@ object MDirectionHeadSignFinder { directionRouteIdInt?.let { routeIdInt -> val routeDirection = routeGTFS.getRouteDirection(routeIdInt, directionId) ?: return@let - routeDirection.destination + routeDirection.destinationOrDirection ?.let { agencyTools.cleanDirectionHeadsign(routeGTFS.getRoute(routeIdInt), directionId, false, false, true, it) } ?.takeIf { it.isNotBlank() } ?.let { From e910ecee9f7ed272b344d2f3abf38a614fb7a302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 14:53:30 -0400 Subject: [PATCH 07/10] wip --- .../java/org/mtransit/parser/gtfs/data/GDirection.kt | 8 ++++++-- .../org/mtransit/parser/mt/GenerateMObjectsTask.java | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt index 24165b6a..065725b8 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt @@ -58,8 +58,10 @@ data class GDirection( private const val ROUTE_ID = GRoute.ROUTE_ID private const val DIRECTION_ID = "direction_id" - private const val DIRECTION = "direction" + private const val DIRECTION = "direction" // can be parsed to enum or regular string (TODO enum useless?) + + private const val DIRECTION_DESTINATION = "direction_destination" private const val DESTINATION = "destination" private const val DIRECTION_NAME = "direction_name" private const val ROUTE_DIRECTION_NAME = "route_direction_name" @@ -77,7 +79,9 @@ data class GDirection( destination = line[DESTINATION]?.trim() ?: line[DIRECTION_NAME]?.trim() ?: line[ROUTE_DIRECTION_NAME]?.trim() - ?: line[DIRECTION_LEGACY]?.trim(), + ?: line[DIRECTION_DESTINATION]?.trim() + ?: line[DIRECTION_LEGACY]?.trim() + ?: line[DIRECTION]?.trim(), ) @JvmStatic diff --git a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java index 8047b8bb..112dc1fa 100644 --- a/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java +++ b/src/main/java/org/mtransit/parser/mt/GenerateMObjectsTask.java @@ -420,9 +420,13 @@ private void fixRouteLongName(HashMap mRoutes, HashMap 0) sb.append(" <> "); - sb.append(mDirection.getHeadsignValue()); + if (mDirection.getHeadsignType() == MDirection.HEADSIGN_TYPE_STRING + && !mDirection.getHeadsignValue().isBlank()) { + final String cleanDirectionHeadsign = agencyTools.cleanRouteLongName(mDirection.getHeadsignValue()); + if (!cleanDirectionHeadsign.isBlank()) { + if (sb.length() > 0) sb.append(" <> "); + sb.append(cleanDirectionHeadsign); + } } } } From 4d83818a81677edf065d6c77c04766fcd5096def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 15:37:20 -0400 Subject: [PATCH 08/10] cleanup --- src/main/java/org/mtransit/parser/DefaultAgencyTools.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index ff4f6b02..3b477be6 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -69,7 +69,7 @@ public class DefaultAgencyTools implements GAgencyTools { static { - CommonsApp.setup(false); + CommonsApp.setup(false); // isAndroidPlatform } @SuppressWarnings("WeakerAccess") From 727b6a659947d7222af20ca9e1730aa572d32c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 20:47:22 -0400 Subject: [PATCH 09/10] PR comments --- .../java/org/mtransit/parser/DefaultAgencyTools.java | 3 ++- .../java/org/mtransit/parser/gtfs/data/GDirection.kt | 12 ++++++------ .../mtransit/parser/mt/MDirectionHeadSignFinder.kt | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java index 3b477be6..90dd1519 100644 --- a/src/main/java/org/mtransit/parser/DefaultAgencyTools.java +++ b/src/main/java/org/mtransit/parser/DefaultAgencyTools.java @@ -1003,7 +1003,8 @@ public boolean removeRouteDescFromDirectionHeadsign() { public @NotNull String cleanDirectionHeadsign(@Nullable GRoute gRoute, int directionId, boolean fromStopName, boolean fromTripHeadSign, boolean fromDirection, @NotNull String directionHeadSign) { if (fromDirection) { //noinspection DiscouragedApi - if (gRoute != null && Configs.getRouteConfig().isDirectionHeadsignIgnoreProvidedDirection(getTodayDateInt(), gRoute.getOriginalRouteId())) { + final String originalRouteId = gRoute == null ? null : gRoute.getOriginalRouteId(); + if (Configs.getRouteConfig().isDirectionHeadsignIgnoreProvidedDirection(getTodayDateInt(), originalRouteId)) { return EMPTY; } } diff --git a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt index 065725b8..934f9ff2 100644 --- a/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt +++ b/src/main/java/org/mtransit/parser/gtfs/data/GDirection.kt @@ -76,12 +76,12 @@ data class GDirection( ?: throw MTLog.Fatal("Invalid GDirection from $line!"), directionIdInt = line[DIRECTION_ID]?.toIntOrNull() ?: throw MTLog.Fatal("Invalid GDirection from $line!"), directionTypeValue = line[DIRECTION]?.trim(), - destination = line[DESTINATION]?.trim() - ?: line[DIRECTION_NAME]?.trim() - ?: line[ROUTE_DIRECTION_NAME]?.trim() - ?: line[DIRECTION_DESTINATION]?.trim() - ?: line[DIRECTION_LEGACY]?.trim() - ?: line[DIRECTION]?.trim(), + destination = line[DESTINATION]?.trim()?.takeIf { it.isNotBlank() } + ?: line[DIRECTION_NAME]?.trim()?.takeIf { it.isNotBlank() } + ?: line[ROUTE_DIRECTION_NAME]?.trim()?.takeIf { it.isNotBlank() } + ?: line[DIRECTION_DESTINATION]?.trim()?.takeIf { it.isNotBlank() } + ?: line[DIRECTION_LEGACY]?.trim()?.takeIf { it.isNotBlank() } + ?: line[DIRECTION]?.trim()?.takeIf { it.isNotBlank() }, ) @JvmStatic diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index 2d860c6a..58f784db 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -46,9 +46,9 @@ object MDirectionHeadSignFinder { MTLog.logDebug("$routeId: $directionId: no trips -> no head-sign.") return@forEach } - val directionRouteIdInt = gRouteTrips.firstOrNull()?.routeIdInt + val directionRouteIdInt = directionTrips.firstOrNull()?.routeIdInt ?.takeIf { firstTripRouteIdInt -> - gRouteTrips.all { it.routeIdInt == firstTripRouteIdInt } + directionTrips.all { it.routeIdInt == firstTripRouteIdInt } } MTLog.logDebug("$routeId: $directionId: direction original route ID(s): ${GIDs.toStringPlus(directionRouteIdInt)} (trips: ${directionTrips.size})") directionRouteIdInt?.let { routeIdInt -> From 6b8886aeda7a0468ee2af829708a1a89348645d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20M=C3=A9a?= Date: Wed, 8 Jul 2026 21:00:51 -0400 Subject: [PATCH 10/10] PR comments --- .../org/mtransit/parser/gtfs/GReader.java | 54 +++++++++---------- .../parser/mt/MDirectionHeadSignFinder.kt | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/mtransit/parser/gtfs/GReader.java b/src/main/java/org/mtransit/parser/gtfs/GReader.java index 38a19a24..a085c0e4 100644 --- a/src/main/java/org/mtransit/parser/gtfs/GReader.java +++ b/src/main/java/org/mtransit/parser/gtfs/GReader.java @@ -273,50 +273,50 @@ private static void readCsv( MTLog.log("Reading file '%s'... > remove 1st empty car", filename); line = String.copyValueOf(line.toCharArray(), 1, line.length() - 1); } - CSVRecord recordColumns = CSVParser.parse(line, CSV_FORMAT).getRecords().get(0); - columnNames = new String[recordColumns.size()]; - for (int i = 0; i < recordColumns.size(); i++) { - columnNames[i] = recordColumns.get(i); + CSVRecord lineRecordColumns = CSVParser.parse(line, CSV_FORMAT).getRecords().get(0); + columnNames = new String[lineRecordColumns.size()]; + for (int i = 0; i < lineRecordColumns.size(); i++) { + columnNames[i] = lineRecordColumns.get(i); } if (onColumnNamesFoundCallback != null) { onColumnNamesFoundCallback.processColumnNames(Arrays.asList(columnNames)); } if (columnNames.length == 0) return; - List records; - HashMap map = new HashMap<>(); - String[] lineColumns = new String[columnNames.length]; - int recordColumnsSize; + List lineRecords; + final HashMap map = new HashMap<>(); int l = 0; boolean withQuotes; + int warningCount = 0; while ((line = reader.readLine()) != null) { try { try { - records = CSVParser.parse(line, CSV_FORMAT).getRecords(); + lineRecords = CSVParser.parse(line, CSV_FORMAT).getRecords(); withQuotes = true; } catch (Exception e) { - records = CSVParser.parse(line, CSV_FORMAT_NO_QUOTE).getRecords(); + lineRecords = CSVParser.parse(line, CSV_FORMAT_NO_QUOTE).getRecords(); withQuotes = false; } - if (records.isEmpty()) { - continue; // empty line - } - recordColumns = records.get(0); - recordColumnsSize = recordColumns.size(); - if (recordColumnsSize > columnNames.length) { - MTLog.log("File '%s' line contains MORE columns (%s:%s) than expected (%s:%s)!", filename, recordColumnsSize, line, columnNames.length, Arrays.asList(columnNames)); - } else if (recordColumnsSize < columnNames.length) { - MTLog.log("File '%s' line contains LESS columns (%s:%s) than expected (%s:%s)!", filename, recordColumnsSize, line, columnNames.length, Arrays.asList(columnNames)); + if (lineRecords.isEmpty()) continue; // empty line + lineRecordColumns = lineRecords.get(0); + // recordColumnsSize = lineRecordColumns.size(); + if (lineRecordColumns.size() > columnNames.length) { + if (warningCount < 10) { + MTLog.log("File '%s' line contains MORE columns (%s:%s) than expected (%s:%s)!", filename, lineRecordColumns.size(), line, columnNames.length, Arrays.asList(columnNames)); + warningCount++; + } + } else if (lineRecordColumns.size() < columnNames.length) { + if (warningCount < 10) { + MTLog.log("File '%s' line contains LESS columns (%s:%s) than expected (%s:%s)!", filename, lineRecordColumns.size(), line, columnNames.length, Arrays.asList(columnNames)); + warningCount++; + } } - for (int i = 0; i < lineColumns.length; i++) { - final String lineColumn = i >= recordColumns.size() ? EMPTY : recordColumns.get(i); - lineColumns[i] = withQuotes ? + map.clear(); + for (int i = 0; i < columnNames.length; i++) { + String lineColumn = i < lineRecordColumns.size() ? lineRecordColumns.get(i) : EMPTY; + lineColumn = withQuotes ? lineColumn : QUOTE_.matcher(lineColumn).replaceAll(EMPTY); - } - map.clear(); - final int usedColumnsCounts = Math.min(recordColumnsSize, columnNames.length); - for (int ci = 0; ci < usedColumnsCounts; ++ci) { - map.put(columnNames[ci], lineColumns[ci]); + map.put(columnNames[i], lineColumn); } if (lineProcessor != null) { lineProcessor.processLine(map); diff --git a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt index 58f784db..db1a144e 100644 --- a/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt +++ b/src/main/java/org/mtransit/parser/mt/MDirectionHeadSignFinder.kt @@ -41,7 +41,7 @@ object MDirectionHeadSignFinder { val directionRouteIdInts = mutableMapOf>() GDirectionId.entries.forEach { gDirectionId -> val directionId = gDirectionId.id - val directionTrips = gRouteTrips.filter { it.directionId == directionId } + val directionTrips = gRouteTrips.filter { gTrip -> gTrip.directionIdOrDefault == directionId } if (directionTrips.isEmpty()) { MTLog.logDebug("$routeId: $directionId: no trips -> no head-sign.") return@forEach