Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 164 additions & 4 deletions src/gtfs/__tests__/transfers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import { Timetable, TransferTypes } from '../../timetable/timetable.js';
import { encode } from '../../timetable/tripStopId.js';
import { GtfsStopsMap } from '../stops.js';
import {
addGeneratedTransfers,
addMissingSiblingTransfers,
buildTripTransfers,
ForbiddenTransfersMap,
GtfsTripTransfer,
parseTransfers,
TransfersMap,
} from '../transfers.js';
import { TripsMapping } from '../trips.js';

Expand Down Expand Up @@ -96,13 +100,15 @@ describe('GTFS transfers parser', () => {
assert.deepEqual(result.tripContinuations, []);
});

it('should ignore impossible transfer types (3 and 5)', async () => {
it('should only retain unscoped impossible transfers as forbidden', async () => {
const mockedStream = new Readable();
mockedStream.push(
'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n',
'from_stop_id,to_stop_id,from_trip_id,to_trip_id,from_route_id,to_route_id,transfer_type,min_transfer_time\n',
);
mockedStream.push('"1100084","8014440:0:1","3","180"\n');
mockedStream.push('"1100097","8014447","5","240"\n');
mockedStream.push('"1100084","8014440:0:1","","","","","3","180"\n');
mockedStream.push('"8014440:0:1","1100084","trip-a","","","","3","180"\n');
mockedStream.push('"1100097","8014447","","","","route-b","3","240"\n');
mockedStream.push('"1100097","8014447","","","","","5","240"\n');
mockedStream.push(null);

const stopsMap: GtfsStopsMap = new Map([
Expand Down Expand Up @@ -151,6 +157,7 @@ describe('GTFS transfers parser', () => {
const result = await parseTransfers(mockedStream, stopsMap, new Set());

assert.deepEqual(result.transfers, new Map());
assert.deepEqual(result.forbiddenTransfers, new Map([[0, new Set([1])]]));
assert.deepEqual(result.tripContinuations, []);
});

Expand Down Expand Up @@ -881,6 +888,159 @@ describe('GTFS transfers parser', () => {
});
});

describe('generated transfers', () => {
const stopsMap: GtfsStopsMap = new Map([
[
'station',
{
id: 0,
sourceStopId: 'station',
name: 'Interchange',
children: [1, 2],
locationType: 'STATION',
},
],
[
'platform-a',
{
id: 1,
sourceStopId: 'platform-a',
name: 'Interchange',
parent: 0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
'platform-b',
{
id: 2,
sourceStopId: 'platform-b',
name: 'Interchange',
parent: 0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);

it('adds directed fallback transfers between active sibling platforms', () => {
const transfers: TransfersMap = new Map();

const added = addMissingSiblingTransfers(
stopsMap,
new Set([1, 2]),
transfers,
);

assert.strictEqual(added, 2);
assert.deepStrictEqual(
transfers,
new Map([
[
1,
[
{
destination: 2,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
},
],
],
[
2,
[
{
destination: 1,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
},
],
],
]),
);
});

it('preserves explicit transfers and excludes forbidden sibling directions', () => {
const transfers: TransfersMap = new Map([
[
1,
[
{
destination: 2,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
minTransferTime: 7,
},
],
],
]);
const forbiddenTransfers: ForbiddenTransfersMap = new Map([
[2, new Set([1])],
]);

const added = addMissingSiblingTransfers(
stopsMap,
new Set([1, 2]),
transfers,
forbiddenTransfers,
);

assert.strictEqual(added, 0);
assert.deepStrictEqual(transfers.get(1), [
{
destination: 2,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
minTransferTime: 7,
},
]);
assert.strictEqual(transfers.has(2), false);
});

it('excludes forbidden directions from generated transfers', () => {
const transfers: TransfersMap = new Map();
const generatedTransfers: TransfersMap = new Map([
[
1,
[
{
destination: 2,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
minTransferTime: 3,
},
],
],
[
2,
[
{
destination: 1,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
minTransferTime: 3,
},
],
],
]);
const forbiddenTransfers: ForbiddenTransfersMap = new Map([
[1, new Set([2])],
]);

const added = addGeneratedTransfers(
generatedTransfers,
new Set([1, 2]),
transfers,
forbiddenTransfers,
);

assert.strictEqual(added, 1);
assert.strictEqual(transfers.has(1), false);
assert.deepStrictEqual(transfers.get(2), [
{
destination: 1,
type: TransferTypes.REQUIRES_MINIMAL_TIME,
minTransferTime: 3,
},
]);
});
});

describe('buildTripTransfers', () => {
it('should build trip transfers for valid data', () => {
const tripsMapping: TripsMapping = new Map([
Expand Down
51 changes: 32 additions & 19 deletions src/gtfs/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { StopId } from '../stops/stops.js';
import { StopsIndex } from '../stops/stopsIndex.js';
import { RouteType, Timetable } from '../timetable/timetable.js';
import { TransferGenerator } from '../transfers/generator.js';
import { getOrInsert } from '../utils/map.js';
import { FrequenciesMap, parseFrequencies } from './frequencies.js';
import { standardGtfsProfile } from './profiles/standard.js';
import { indexRoutes, parseRoutes } from './routes.js';
import { parseCalendar, parseCalendarDates, ServiceIds } from './services.js';
import { parseStops } from './stops.js';
import {
addGeneratedTransfers,
addMissingSiblingTransfers,
buildTripTransfers,
ForbiddenTransfersMap,
GtfsTripTransfer,
parseTransfers,
TransfersMap,
Expand All @@ -36,6 +38,12 @@ const TRANSFERS_FILE = 'transfers.txt';

export type GtfsProfile = {
routeTypeParser: (routeType: number) => Maybe<RouteType>;
/**
* Derive fallback transfers between active stops that share a parent station.
*
* @default true
*/
deriveSiblingTransfers?: boolean;
};

export class GtfsParser {
Expand Down Expand Up @@ -123,6 +131,7 @@ export class GtfsParser {
);

let transfers: TransfersMap = new Map();
let forbiddenTransfers: ForbiddenTransfersMap = new Map();
let tripContinuationsList: GtfsTripTransfer[] = [];
let guaranteedTripTransfersList: GtfsTripTransfer[] = [];
if (entries[TRANSFERS_FILE]) {
Expand All @@ -131,10 +140,12 @@ export class GtfsParser {
const transfersStream = await zip.stream(TRANSFERS_FILE);
const {
transfers: parsedTransfers,
forbiddenTransfers: parsedForbiddenTransfers,
tripContinuations: parsedTripContinuations,
guaranteedTripTransfers: parsedGuaranteedTripTransfers,
} = await parseTransfers(transfersStream, parsedStops, activeServiceIds);
transfers = parsedTransfers;
forbiddenTransfers = parsedForbiddenTransfers;
tripContinuationsList = parsedTripContinuations;
guaranteedTripTransfersList = parsedGuaranteedTripTransfers;
const transfersEnd = performance.now();
Expand Down Expand Up @@ -174,6 +185,20 @@ export class GtfsParser {
`${routes.length} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`,
);

if (this.profile.deriveSiblingTransfers !== false) {
const siblingTransfersStart = performance.now();
const siblingTransfersAdded = addMissingSiblingTransfers(
parsedStops,
activeStopIds,
transfers,
forbiddenTransfers,
);
const siblingTransfersEnd = performance.now();
log.info(
`${siblingTransfersAdded} sibling transfers added. (${(siblingTransfersEnd - siblingTransfersStart).toFixed(2)}ms)`,
);
}

if (this.transferGenerator) {
log.info('Generating virtual transfers');
const virtualTransfersStart = performance.now();
Expand All @@ -190,24 +215,12 @@ export class GtfsParser {
originStops,
stopsIndex,
);
let addedTransfers = 0;
for (const [fromStop, newTransfers] of generatedTransfers) {
const existing = getOrInsert(transfers, fromStop, []);
// Deduplicate per directed pair against existing (feed) transfers, and
// only keep transfers into stops a route actually calls at.
const connected = new Set(existing.map((t) => t.destination));
for (const transfer of newTransfers) {
if (
!activeStopIds.has(transfer.destination) ||
connected.has(transfer.destination)
) {
continue;
}
connected.add(transfer.destination);
existing.push(transfer);
addedTransfers += 1;
}
}
const addedTransfers = addGeneratedTransfers(
generatedTransfers,
activeStopIds,
transfers,
forbiddenTransfers,
);
const virtualTransfersEnd = performance.now();
log.info(
`${addedTransfers} virtual transfers added. (${(virtualTransfersEnd - virtualTransfersStart).toFixed(2)}ms)`,
Expand Down
4 changes: 4 additions & 0 deletions src/gtfs/profiles/__tests__/extended.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { RouteTypes } from '../../../timetable/timetable.js';
import { extendedGtfsProfile } from '../extended.js';

describe('The extended GTFS feed parser', () => {
it('derives sibling transfers by default', () => {
assert.strictEqual(extendedGtfsProfile.deriveSiblingTransfers, true);
});

it('should convert the extended route type to GTFS route type', () => {
assert.ok(extendedGtfsProfile.routeTypeParser);
assert.equal(extendedGtfsProfile.routeTypeParser(106), RouteTypes.RAIL);
Expand Down
10 changes: 10 additions & 0 deletions src/gtfs/profiles/__tests__/standard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';

import { standardGtfsProfile } from '../standard.js';

describe('The standard GTFS feed parser', () => {
it('derives sibling transfers by default', () => {
assert.strictEqual(standardGtfsProfile.deriveSiblingTransfers, true);
});
});
1 change: 1 addition & 0 deletions src/gtfs/profiles/extended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,6 @@ const routeTypeParser = (routeType: number): Maybe<RouteType> => {
};

export const extendedGtfsProfile: GtfsProfile = {
deriveSiblingTransfers: true,
routeTypeParser,
};
1 change: 1 addition & 0 deletions src/gtfs/profiles/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RouteTypes } from '../../timetable/timetable.js';
import { GtfsProfile } from '../parser.js';

export const standardGtfsProfile: GtfsProfile = {
deriveSiblingTransfers: true,
routeTypeParser: (routeType: number) => {
switch (routeType) {
case 0:
Expand Down
Loading
Loading