minotor
Version:
A lightweight client-side transit routing library.
535 lines (505 loc) • 15 kB
text/typescript
import assert from 'node:assert';
import { Readable } from 'node:stream';
import { describe, it } from 'node:test';
import { StopId } from '../../stops/stops.js';
import { Time } from '../../timetable/time.js';
import {
RoutesAdjacency,
ServiceRoutesMap,
} from '../../timetable/timetable.js';
import { ServiceIds } from '../services.js';
import { ParsedStopsMap } from '../stops.js';
import { TransfersMap } from '../transfers.js';
import {
buildStopsAdjacencyStructure,
parseStopTimes,
parseTrips,
TripIdsMap,
} from '../trips.js';
describe('buildStopsAdjacencyStructure', () => {
it('should correctly build stops adjacency for valid routes and transfers', () => {
const validStops: Set<StopId> = new Set([0]);
const routesAdjacency: RoutesAdjacency = new Map([
[
'routeA',
{
serviceRouteId: 'service1',
stops: new Uint32Array([0, 1]),
stopIndices: new Map([
[0, 0],
[1, 1],
]),
stopTimes: new Uint16Array(),
pickUpDropOffTypes: new Uint8Array(),
},
],
]);
const transfersMap: TransfersMap = new Map([
[0, [{ destination: 1, type: 'RECOMMENDED' }]],
]);
const stopsAdjacency = buildStopsAdjacencyStructure(
validStops,
routesAdjacency,
transfersMap,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: ['routeA'],
transfers: [],
},
],
]);
});
it('should ignore transfers to invalid stops', () => {
const validStops: Set<StopId> = new Set([0, 1]);
const routesAdjacency: RoutesAdjacency = new Map([
[
'routeA',
{
serviceRouteId: 'service1',
stops: new Uint32Array([0, 1]),
stopIndices: new Map([
[0, 0],
[1, 1],
]),
stopTimes: new Uint16Array(),
pickUpDropOffTypes: new Uint8Array(),
},
],
]);
const transfersMap: TransfersMap = new Map([
[0, [{ destination: 2, type: 'RECOMMENDED' }]],
]);
const stopsAdjacency = buildStopsAdjacencyStructure(
validStops,
routesAdjacency,
transfersMap,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: ['routeA'],
transfers: [],
},
],
[
1,
{
routes: ['routeA'],
transfers: [],
},
],
]);
});
});
describe('GTFS trips parser', () => {
it('should correctly parse valid trips', async () => {
const mockedStream = new Readable();
mockedStream.push('route_id,service_id,trip_id\n');
mockedStream.push('"routeA","service1","trip1"\n');
mockedStream.push('"routeB","service2","trip2"\n');
mockedStream.push(null);
const validServiceIds: ServiceIds = new Set(['service1', 'service2']);
const validRouteIds: ServiceRoutesMap = new Map([
['routeA', { type: 'BUS', name: 'B1' }],
['routeB', { type: 'TRAM', name: 'T1' }],
]);
const trips = await parseTrips(
mockedStream,
validServiceIds,
validRouteIds,
);
assert.deepEqual(
trips,
new Map([
['trip1', 'routeA'],
['trip2', 'routeB'],
]),
);
});
it('should ignore trips with invalid service ids', async () => {
const mockedStream = new Readable();
mockedStream.push('route_id,service_id,trip_id\n');
mockedStream.push('"routeA","service1","trip1"\n');
mockedStream.push('"routeB","service3","trip2"\n');
mockedStream.push(null);
const validServiceIds: ServiceIds = new Set(['service1', 'service2']);
const validRouteIds: ServiceRoutesMap = new Map([
['routeA', { type: 'BUS', name: 'B1' }],
['routeB', { type: 'TRAM', name: 'T1' }],
]);
const trips = await parseTrips(
mockedStream,
validServiceIds,
validRouteIds,
);
assert.deepEqual(trips, new Map([['trip1', 'routeA']]));
});
it('should ignore trips with invalid route ids', async () => {
const mockedStream = new Readable();
mockedStream.push('route_id,service_id,trip_id\n');
mockedStream.push('"routeA","service1","trip1"\n');
mockedStream.push('"routeC","service2","trip2"\n');
mockedStream.push(null);
const validServiceIds: ServiceIds = new Set(['service1', 'service2']);
const validRouteIds: ServiceRoutesMap = new Map([
['routeA', { type: 'BUS', name: 'B1' }],
['routeB', { type: 'TRAM', name: 'T1' }],
]);
const trips = await parseTrips(
mockedStream,
validServiceIds,
validRouteIds,
);
assert.deepEqual(trips, new Map([['trip1', 'routeA']]));
});
});
describe('GTFS stop times parser', () => {
it('should correctly parse valid stop times', async () => {
const mockedStream = new Readable();
mockedStream.push(
'trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\n',
);
mockedStream.push('"tripA","08:00:00","08:05:00","stop1","1","0","0"\n');
mockedStream.push('"tripA","08:10:00","08:15:00","stop2","2","0","0"\n');
mockedStream.push(null);
const validTripIds: TripIdsMap = new Map([['tripA', 'routeA']]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: ParsedStopsMap = new Map([
[
'stop1',
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
lat: 36.425288,
lon: -117.133162,
},
],
[
'stop2',
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
lat: 36.868446,
lon: -116.784582,
},
],
]);
const routes = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(
routes,
new Map([
[
'routeA_1',
{
serviceRouteId: 'routeA',
stops: new Uint32Array([0, 1]),
stopIndices: new Map([
[0, 0],
[1, 1],
]),
stopTimes: new Uint16Array([
Time.fromHMS(8, 0, 0).toMinutes(),
Time.fromHMS(8, 5, 0).toMinutes(),
Time.fromHMS(8, 10, 0).toMinutes(),
Time.fromHMS(8, 15, 0).toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([0, 0, 0, 0]),
},
],
]),
);
});
it('should create same route for same GTFS route with same stops', async () => {
const mockedStream = new Readable();
mockedStream.push(
'trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\n',
);
mockedStream.push('"tripA","08:00:00","08:05:00","stop1","1","0","0"\n');
mockedStream.push('"tripA","08:10:00","08:15:00","stop2","2","0","0"\n');
mockedStream.push('"tripB","09:00:00","09:05:00","stop1","1","0","0"\n');
mockedStream.push('"tripB","09:10:00","09:15:00","stop2","2","0","0"\n');
mockedStream.push(null);
const validTripIds: TripIdsMap = new Map([
['tripA', 'routeA'],
['tripB', 'routeA'],
]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: ParsedStopsMap = new Map([
[
'stop1',
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
'stop2',
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const routes = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(
routes,
new Map([
[
'routeA_1',
{
serviceRouteId: 'routeA',
stops: new Uint32Array([0, 1]),
stopIndices: new Map([
[0, 0],
[1, 1],
]),
stopTimes: new Uint16Array([
Time.fromHMS(8, 0, 0).toMinutes(),
Time.fromHMS(8, 5, 0).toMinutes(),
Time.fromHMS(8, 10, 0).toMinutes(),
Time.fromHMS(8, 15, 0).toMinutes(),
Time.fromHMS(9, 0, 0).toMinutes(),
Time.fromHMS(9, 5, 0).toMinutes(),
Time.fromHMS(9, 10, 0).toMinutes(),
Time.fromHMS(9, 15, 0).toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]),
},
],
]),
);
});
it('should support unsorted trips within a route', async () => {
const mockedStream = new Readable();
mockedStream.push(
'trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\n',
);
mockedStream.push('"tripB","09:00:00","09:05:00","stop1","1","0","0"\n');
mockedStream.push('"tripB","09:10:00","09:15:00","stop2","2","0","0"\n');
mockedStream.push('"tripA","08:00:00","08:05:00","stop1","1","0","0"\n');
mockedStream.push('"tripA","08:10:00","08:15:00","stop2","2","0","0"\n');
mockedStream.push(null);
const validTripIds: TripIdsMap = new Map([
['tripA', 'routeA'],
['tripB', 'routeA'],
]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: ParsedStopsMap = new Map([
[
'stop1',
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
'stop2',
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const routes = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(
routes,
new Map([
[
'routeA_1',
{
serviceRouteId: 'routeA',
stops: new Uint32Array([0, 1]),
stopIndices: new Map([
[0, 0],
[1, 1],
]),
stopTimes: new Uint16Array([
Time.fromHMS(8, 0, 0).toMinutes(),
Time.fromHMS(8, 5, 0).toMinutes(),
Time.fromHMS(8, 10, 0).toMinutes(),
Time.fromHMS(8, 15, 0).toMinutes(),
Time.fromHMS(9, 0, 0).toMinutes(),
Time.fromHMS(9, 5, 0).toMinutes(),
Time.fromHMS(9, 10, 0).toMinutes(),
Time.fromHMS(9, 15, 0).toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]),
},
],
]),
);
});
it('should create distinct route for same GTFS route with different stops', async () => {
const mockedStream = new Readable();
mockedStream.push(
'trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\n',
);
mockedStream.push('"tripA","08:00:00","08:05:00","stop1","1","0","0"\n');
mockedStream.push('"tripA","08:10:00","08:15:00","stop2","2","0","0"\n');
mockedStream.push('"tripB","09:00:00","09:15:00","stop1","1","0","0"\n');
mockedStream.push(null);
const validTripIds: TripIdsMap = new Map([
['tripA', 'routeA'],
['tripB', 'routeA'],
]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: ParsedStopsMap = new Map([
[
'stop1',
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
'stop2',
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const routes = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(
routes,
new Map([
[
'routeA_1',
{
serviceRouteId: 'routeA',
stops: new Uint32Array([0, 1]),
stopIndices: new Map([
[0, 0],
[1, 1],
]),
stopTimes: new Uint16Array([
Time.fromHMS(8, 0, 0).toMinutes(),
Time.fromHMS(8, 5, 0).toMinutes(),
Time.fromHMS(8, 10, 0).toMinutes(),
Time.fromHMS(8, 15, 0).toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([0, 0, 0, 0]),
},
],
[
'routeA_0',
{
serviceRouteId: 'routeA',
stops: new Uint32Array([0]),
stopIndices: new Map([[0, 0]]),
stopTimes: new Uint16Array([
Time.fromHMS(9, 0, 0).toMinutes(),
Time.fromHMS(9, 15, 0).toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([0, 0]),
},
],
]),
);
});
it('should ignore non-increasing stop sequences', async () => {
const mockedStream = new Readable();
mockedStream.push(
'trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type\n',
);
mockedStream.push('"tripA","08:00:00","08:05:00","stop1","2","0","0"\n');
mockedStream.push('"tripA","08:10:00","08:15:00","stop2","1","0","0"\n');
mockedStream.push(null);
const validTripIds: TripIdsMap = new Map([['tripA', 'routeA']]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: ParsedStopsMap = new Map([
[
'stop1',
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
'stop2',
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const routes = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(
routes,
new Map([
[
'routeA_0',
{
serviceRouteId: 'routeA',
stops: new Uint32Array([0]),
stopIndices: new Map([[0, 0]]),
stopTimes: new Uint16Array([
Time.fromHMS(8, 0, 0).toMinutes(),
Time.fromHMS(8, 5, 0).toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([0, 0]),
},
],
]),
);
});
});