From bc6f84af979c465ce5e3353f9732ffa61abe846d Mon Sep 17 00:00:00 2001 From: Aubry Cholleton Date: Fri, 24 Jul 2026 20:07:47 +0200 Subject: [PATCH] feat: support for service_ids in transfers.txt --- src/gtfs/__tests__/transfers.test.ts | 113 ++++++++++++++++++++++++--- src/gtfs/parser.ts | 2 +- src/gtfs/transfers.ts | 16 +++- 3 files changed, 115 insertions(+), 16 deletions(-) diff --git a/src/gtfs/__tests__/transfers.test.ts b/src/gtfs/__tests__/transfers.test.ts index 1cf906f..ac0e88a 100644 --- a/src/gtfs/__tests__/transfers.test.ts +++ b/src/gtfs/__tests__/transfers.test.ts @@ -67,7 +67,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); const expectedTransfers = new Map([ [ @@ -148,7 +148,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); assert.deepEqual(result.transfers, new Map()); assert.deepEqual(result.tripContinuations, []); @@ -186,7 +186,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); assert.deepEqual(result.transfers, new Map()); assert.deepEqual(result.tripContinuations, []); @@ -244,7 +244,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); const expectedTripContinuations = [ { @@ -318,7 +318,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); const expectedGuaranteedTripTransfers = [ { @@ -398,7 +398,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); // Type 1 with trip IDs -> guaranteedTripTransfers const expectedGuaranteedTripTransfers = [ @@ -486,7 +486,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); assert.deepEqual(result.transfers, new Map()); assert.deepEqual(result.tripContinuations, []); @@ -523,7 +523,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); assert.deepEqual(result.transfers, new Map()); assert.deepEqual(result.tripContinuations, []); @@ -560,7 +560,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); assert.deepEqual(result.transfers, new Map()); assert.deepEqual(result.tripContinuations, []); @@ -618,7 +618,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); const expectedTransfers = new Map([ [ @@ -698,7 +698,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); const expectedTransfers = new Map([ [ @@ -726,6 +726,93 @@ describe('GTFS transfers parser', () => { assert.deepEqual(result.tripContinuations, expectedTripContinuations); }); + it('should filter service-scoped transfers by active service regardless of type', async () => { + const mockedStream = new Readable(); + mockedStream.push( + 'from_stop_id,to_stop_id,from_trip_id,to_trip_id,service_id,transfer_type,min_transfer_time\n', + ); + mockedStream.push('"from","to","","","","0",""\n'); + mockedStream.push('"from","to","","","","2","120"\n'); + mockedStream.push('"from","to","","","active","1",""\n'); + mockedStream.push('"from","to","","","inactive","2","180"\n'); + mockedStream.push('"from","to","tripA","tripB","active","1",""\n'); + mockedStream.push('"from","to","tripC","tripD","inactive","1",""\n'); + mockedStream.push('"from","to","tripE","tripF","active","4",""\n'); + mockedStream.push('"from","to","tripG","tripH","inactive","4",""\n'); + mockedStream.push(null); + + const stopsMap: GtfsStopsMap = new Map([ + [ + 'from', + { + id: 0, + sourceStopId: 'from', + name: 'From', + children: [], + locationType: 'SIMPLE_STOP_OR_PLATFORM', + }, + ], + [ + 'to', + { + id: 1, + sourceStopId: 'to', + name: 'To', + children: [], + locationType: 'SIMPLE_STOP_OR_PLATFORM', + }, + ], + ]); + + const result = await parseTransfers( + mockedStream, + stopsMap, + new Set(['active']), + ); + + assert.deepEqual( + result.transfers, + new Map([ + [ + 0, + [ + { + destination: 1, + type: TransferTypes.RECOMMENDED, + minTransferTime: durationFromSeconds(0), + }, + { + destination: 1, + type: TransferTypes.REQUIRES_MINIMAL_TIME, + minTransferTime: durationFromSeconds(120), + }, + { + destination: 1, + type: TransferTypes.GUARANTEED, + minTransferTime: durationFromSeconds(0), + }, + ], + ], + ]), + ); + assert.deepEqual(result.guaranteedTripTransfers, [ + { + fromStop: 0, + fromTrip: 'tripA', + toStop: 1, + toTrip: 'tripB', + }, + ]); + assert.deepEqual(result.tripContinuations, [ + { + fromStop: 0, + fromTrip: 'tripE', + toStop: 1, + toTrip: 'tripF', + }, + ]); + }); + it('should handle empty transfers file', async () => { const mockedStream = new Readable(); mockedStream.push( @@ -735,7 +822,7 @@ describe('GTFS transfers parser', () => { const stopsMap: GtfsStopsMap = new Map(); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); assert.deepEqual(result.transfers, new Map()); assert.deepEqual(result.tripContinuations, []); @@ -774,7 +861,7 @@ describe('GTFS transfers parser', () => { ], ]); - const result = await parseTransfers(mockedStream, stopsMap); + const result = await parseTransfers(mockedStream, stopsMap, new Set()); const expectedTransfers = new Map([ [ diff --git a/src/gtfs/parser.ts b/src/gtfs/parser.ts index 31418de..95d530b 100644 --- a/src/gtfs/parser.ts +++ b/src/gtfs/parser.ts @@ -133,7 +133,7 @@ export class GtfsParser { transfers: parsedTransfers, tripContinuations: parsedTripContinuations, guaranteedTripTransfers: parsedGuaranteedTripTransfers, - } = await parseTransfers(transfersStream, parsedStops); + } = await parseTransfers(transfersStream, parsedStops, activeServiceIds); transfers = parsedTransfers; tripContinuationsList = parsedTripContinuations; guaranteedTripTransfersList = parsedGuaranteedTripTransfers; diff --git a/src/gtfs/transfers.ts b/src/gtfs/transfers.ts index 0014b96..56217fe 100644 --- a/src/gtfs/transfers.ts +++ b/src/gtfs/transfers.ts @@ -13,6 +13,7 @@ import { TripTransfers as TripTransfers, } from '../timetable/timetable.js'; import { encode } from '../timetable/tripStopId.js'; +import { ServiceId, ServiceIds } from './services.js'; import { GtfsStopsMap } from './stops.js'; import { GtfsTripId, TripsMapping } from './trips.js'; import { parseCsv } from './utils.js'; @@ -41,6 +42,7 @@ export type TransferEntry = { to_trip_id?: GtfsTripId; from_route_id?: ServiceRouteId; to_route_id?: ServiceRouteId; + service_id?: ServiceId; transfer_type: GtfsTransferType; min_transfer_time?: number; }; @@ -180,12 +182,15 @@ const processStopToStopTransfer = ( /** * Parses the transfers.txt file from a GTFS feed. * - * @param stopsStream The readable stream containing the stops data. - * @return A mapping of stop IDs to corresponding stop details. + * @param transfersStream The readable stream containing the transfers data. + * @param stopsMap The parsed GTFS stops indexed by their source IDs. + * @param activeServiceIds The service IDs active for the requested date. + * @returns Parsed stop transfers, trip continuations, and guaranteed trip transfers. */ export const parseTransfers = async ( transfersStream: NodeJS.ReadableStream, stopsMap: GtfsStopsMap, + activeServiceIds: ServiceIds, ): Promise<{ transfers: TransfersMap; tripContinuations: GtfsTripTransfer[]; @@ -201,6 +206,13 @@ export const parseTransfers = async ( ])) { const transferEntry = rawLine as TransferEntry; + if ( + transferEntry.service_id && + !activeServiceIds.has(transferEntry.service_id) + ) { + continue; + } + if ( transferEntry.transfer_type === 3 || transferEntry.transfer_type === 5