minotor
Version:
A lightweight client-side transit routing library.
825 lines (781 loc) • 21.1 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 { PickUpDropOffTypes, Route } from '../../timetable/route.js';
import { timeFromHMS } from '../../timetable/time.js';
import {
RouteTypes,
ServiceRoute,
TransferTypes,
} from '../../timetable/timetable.js';
import { GtfsRoutesMap } from '../routes.js';
import { ServiceIds } from '../services.js';
import { GtfsStopsMap } from '../stops.js';
import { TransfersMap } from '../transfers.js';
import {
buildStopsAdjacencyStructure,
encodePickUpDropOffTypes,
GtfsTripIdsMap,
parseStopTimes,
parseTrips,
} from '../trips.js';
describe('buildStopsAdjacencyStructure', () => {
it('should correctly build stops adjacency for valid routes and transfers', () => {
const validStops: Set<StopId> = new Set([0]);
const routes = [
Route.of({
id: 0,
serviceRouteId: 0,
trips: [
{
stops: [
{
id: 0,
arrivalTime: timeFromHMS(8, 0, 0),
departureTime: timeFromHMS(8, 0, 0),
},
{
id: 1,
arrivalTime: timeFromHMS(8, 5, 0),
departureTime: timeFromHMS(8, 5, 0),
},
],
},
],
}),
];
const transfersMap: TransfersMap = new Map([
[0, [{ destination: 1, type: TransferTypes.RECOMMENDED }]],
]);
const serviceRoutes: ServiceRoute[] = [
{ type: RouteTypes.BUS, name: 'B1', routes: [] },
];
const stopsAdjacency = buildStopsAdjacencyStructure(
serviceRoutes,
routes,
transfersMap,
2,
validStops,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: [0],
transfers: [
{
destination: 1,
type: TransferTypes.RECOMMENDED,
},
],
},
],
[
1,
{
routes: [],
},
],
]);
assert.deepEqual(serviceRoutes[0]?.routes, [0]);
});
it('should ignore transfers to invalid stops', () => {
const validStops: Set<StopId> = new Set([0, 1]);
const routes = [
Route.of({
id: 0,
serviceRouteId: 0,
trips: [
{
stops: [
{
id: 0,
arrivalTime: timeFromHMS(8, 0, 0),
departureTime: timeFromHMS(8, 0, 0),
},
{
id: 1,
arrivalTime: timeFromHMS(8, 5, 0),
departureTime: timeFromHMS(8, 5, 0),
},
],
},
],
}),
];
const transfersMap: TransfersMap = new Map([
[3, [{ destination: 2, type: TransferTypes.RECOMMENDED }]],
]);
const serviceRoutes: ServiceRoute[] = [
{ type: RouteTypes.BUS, name: 'B1', routes: [] },
];
const stopsAdjacency = buildStopsAdjacencyStructure(
serviceRoutes,
routes,
transfersMap,
4,
validStops,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: [0],
},
],
[
1,
{
routes: [0],
},
],
[
2,
{
routes: [],
},
],
[
3,
{
routes: [],
},
],
]);
assert.deepEqual(serviceRoutes[0]?.routes, [0]);
});
it('should correctly handle trip continuations', () => {
const validStops: Set<StopId> = new Set([0, 1]);
const routes = [
Route.of({
id: 0,
serviceRouteId: 0,
trips: [
{
stops: [
{
id: 0,
arrivalTime: timeFromHMS(8, 0, 0),
departureTime: timeFromHMS(8, 0, 0),
},
],
},
],
}),
Route.of({
id: 1,
serviceRouteId: 0,
trips: [
{
stops: [
{
id: 1,
arrivalTime: timeFromHMS(8, 30, 0),
departureTime: timeFromHMS(8, 30, 0),
},
],
},
],
}),
];
const transfersMap: TransfersMap = new Map();
const serviceRoutes: ServiceRoute[] = [
{ type: RouteTypes.BUS, name: 'B1', routes: [] },
];
const stopsAdjacency = buildStopsAdjacencyStructure(
serviceRoutes,
routes,
transfersMap,
2,
validStops,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: [0],
},
],
[
1,
{
routes: [1],
},
],
]);
});
it('should ignore trip continuations with invalid trip IDs', () => {
const validStops: Set<StopId> = new Set([0]);
const routes = [
Route.of({
id: 0,
serviceRouteId: 0,
trips: [
{
stops: [
{
id: 0,
arrivalTime: timeFromHMS(8, 0, 0),
departureTime: timeFromHMS(8, 0, 0),
},
],
},
],
}),
];
const transfersMap: TransfersMap = new Map();
const serviceRoutes: ServiceRoute[] = [
{ type: RouteTypes.BUS, name: 'B1', routes: [] },
];
const stopsAdjacency = buildStopsAdjacencyStructure(
serviceRoutes,
routes,
transfersMap,
1,
validStops,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: [0],
},
],
]);
});
it('should ignore trip continuations for inactive stops', () => {
const validStops: Set<StopId> = new Set([0]); // Only stop 0 is active
const routes = [
Route.of({
id: 0,
serviceRouteId: 0,
trips: [
{
stops: [
{
id: 0,
arrivalTime: timeFromHMS(8, 0, 0),
departureTime: timeFromHMS(8, 0, 0),
},
],
},
],
}),
];
const transfersMap: TransfersMap = new Map();
const serviceRoutes: ServiceRoute[] = [
{ type: RouteTypes.BUS, name: 'B1', routes: [] },
];
const stopsAdjacency = buildStopsAdjacencyStructure(
serviceRoutes,
routes,
transfersMap,
4,
validStops,
);
assert.deepEqual(Array.from(stopsAdjacency.entries()), [
[
0,
{
routes: [0],
},
],
[
1,
{
routes: [],
},
],
[
2,
{
routes: [],
},
],
[
3,
{
routes: [],
},
],
]);
});
});
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: GtfsRoutesMap = new Map([
['routeA', { type: RouteTypes.BUS, name: 'B1' }],
['routeB', { type: RouteTypes.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: GtfsRoutesMap = new Map([
['routeA', { type: RouteTypes.BUS, name: 'B1' }],
['routeB', { type: RouteTypes.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: GtfsRoutesMap = new Map([
['routeA', { type: RouteTypes.BUS, name: 'B1' }],
['routeB', { type: RouteTypes.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: GtfsTripIdsMap = new Map([['tripA', 'routeA']]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: GtfsStopsMap = 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 result = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(result.routes, [
new Route(
0,
new Uint16Array([
timeFromHMS(8, 0, 0),
timeFromHMS(8, 5, 0),
timeFromHMS(8, 10, 0),
timeFromHMS(8, 15, 0),
]),
encodePickUpDropOffTypes(
[PickUpDropOffTypes.REGULAR, PickUpDropOffTypes.REGULAR],
[PickUpDropOffTypes.REGULAR, PickUpDropOffTypes.REGULAR],
),
new Uint32Array([0, 1]),
0,
),
]);
assert.deepEqual(
result.tripsMapping,
new Map([['tripA', { routeId: 0, tripRouteIndex: 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: GtfsTripIdsMap = new Map([
['tripA', 'routeA'],
['tripB', 'routeA'],
]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: GtfsStopsMap = 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 result = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(result.routes, [
new Route(
0,
new Uint16Array([
timeFromHMS(8, 0, 0),
timeFromHMS(8, 5, 0),
timeFromHMS(8, 10, 0),
timeFromHMS(8, 15, 0),
timeFromHMS(9, 0, 0),
timeFromHMS(9, 5, 0),
timeFromHMS(9, 10, 0),
timeFromHMS(9, 15, 0),
]),
encodePickUpDropOffTypes(
[
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
],
[
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
],
),
new Uint32Array([0, 1]),
0,
),
]);
assert.deepEqual(
result.tripsMapping,
new Map([
['tripA', { routeId: 0, tripRouteIndex: 0 }],
['tripB', { routeId: 0, tripRouteIndex: 1 }],
]),
);
});
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: GtfsTripIdsMap = new Map([
['tripA', 'routeA'],
['tripB', 'routeA'],
]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: GtfsStopsMap = 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 result = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(result.routes, [
new Route(
0,
new Uint16Array([
timeFromHMS(8, 0, 0),
timeFromHMS(8, 5, 0),
timeFromHMS(8, 10, 0),
timeFromHMS(8, 15, 0),
timeFromHMS(9, 0, 0),
timeFromHMS(9, 5, 0),
timeFromHMS(9, 10, 0),
timeFromHMS(9, 15, 0),
]),
encodePickUpDropOffTypes(
[
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
],
[
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
PickUpDropOffTypes.REGULAR,
],
),
new Uint32Array([0, 1]),
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: GtfsTripIdsMap = new Map([
['tripA', 'routeA'],
['tripB', 'routeA'],
]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: GtfsStopsMap = 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 result = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(result.routes, [
new Route(
0,
new Uint16Array([
timeFromHMS(8, 0, 0),
timeFromHMS(8, 5, 0),
timeFromHMS(8, 10, 0),
timeFromHMS(8, 15, 0),
]),
encodePickUpDropOffTypes(
[PickUpDropOffTypes.REGULAR, PickUpDropOffTypes.REGULAR],
[PickUpDropOffTypes.REGULAR, PickUpDropOffTypes.REGULAR],
),
new Uint32Array([0, 1]),
0,
),
new Route(
1,
new Uint16Array([timeFromHMS(9, 0, 0), timeFromHMS(9, 15, 0)]),
encodePickUpDropOffTypes(
[PickUpDropOffTypes.REGULAR],
[PickUpDropOffTypes.REGULAR],
),
new Uint32Array([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: GtfsTripIdsMap = new Map([['tripA', 'routeA']]);
const validStopIds: Set<StopId> = new Set([0, 1]);
const stopsMap: GtfsStopsMap = 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 result = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(result.routes, [
new Route(
0,
new Uint16Array([timeFromHMS(8, 0, 0), timeFromHMS(8, 5, 0)]),
encodePickUpDropOffTypes(
[PickUpDropOffTypes.REGULAR],
[PickUpDropOffTypes.REGULAR],
),
new Uint32Array([0]),
0,
),
]);
assert.deepEqual(
result.tripsMapping,
new Map([['tripA', { routeId: 0, tripRouteIndex: 0 }]]),
);
});
it('should create trip continuations mapping correctly', 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('"trip1","08:00:00","08:05:00","stop1","1","0","0"\n');
mockedStream.push('"trip1","08:10:00","08:15:00","stop2","2","0","0"\n');
mockedStream.push('"trip2","09:00:00","09:05:00","stop3","1","0","0"\n');
mockedStream.push(null);
const validTripIds: GtfsTripIdsMap = new Map([
['trip1', 'routeA'],
['trip2', 'routeB'],
]);
const validStopIds: Set<StopId> = new Set([0, 1, 2]);
const stopsMap: GtfsStopsMap = 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',
},
],
[
'stop3',
{
id: 2,
sourceStopId: 'stop3',
name: 'Stop 3',
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const result = await parseStopTimes(
mockedStream,
stopsMap,
validTripIds,
validStopIds,
);
assert.deepEqual(
result.tripsMapping,
new Map([
['trip1', { routeId: 0, tripRouteIndex: 0 }],
['trip2', { routeId: 1, tripRouteIndex: 0 }],
]),
);
assert.deepEqual(
result.serviceRoutesMap,
new Map([
['routeA', 0],
['routeB', 1],
]),
);
});
});