minotor
Version:
A lightweight client-side transit routing library.
751 lines (708 loc) • 20.3 kB
text/typescript
import assert from 'node:assert';
import { beforeEach, describe, it } from 'node:test';
import { StopsMap } from '../../stops/stops.js';
import { StopsIndex } from '../../stops/stopsIndex.js';
import { Duration } from '../../timetable/duration.js';
import { Time } from '../../timetable/time.js';
import {
RoutesAdjacency,
ServiceRoutesMap,
StopsAdjacency,
Timetable,
} from '../../timetable/timetable.js';
import { Query } from '../query.js';
import { Result } from '../result.js';
import { Router } from '../router.js';
describe('Router', () => {
describe('with a single route', () => {
let router: Router;
let timetable: Timetable;
beforeEach(() => {
const stopsAdjacency: StopsAdjacency = new Map([
[0, { transfers: [], routes: ['route1'] }],
[1, { transfers: [], routes: ['route1'] }],
[2, { transfers: [], routes: ['route1'] }],
]);
const routesAdjacency: RoutesAdjacency = new Map([
[
'route1',
{
stopTimes: new Uint16Array([
Time.fromString('08:00:00').toMinutes(),
Time.fromString('08:10:00').toMinutes(),
Time.fromString('08:15:00').toMinutes(),
Time.fromString('08:25:00').toMinutes(),
Time.fromString('08:35:00').toMinutes(),
Time.fromString('08:45:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0,
0, // REGULAR
0,
0, // REGULAR
0,
0, // REGULAR
]),
stops: new Uint32Array([0, 1, 2]),
stopIndices: new Map([
[0, 0],
[1, 1],
[2, 2],
]),
serviceRouteId: 'service_route1',
},
],
]);
const routes: ServiceRoutesMap = new Map([
[
'service_route1',
{
type: 'BUS',
name: 'Line 1',
},
],
]);
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
const stopsMap: StopsMap = new Map([
[
0,
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
lat: 1.0,
lon: 1.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
1,
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
lat: 2.0,
lon: 2.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
2,
{
id: 2,
sourceStopId: 'stop3',
name: 'Stop 3',
lat: 3.0,
lon: 3.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const stopsIndex = new StopsIndex(stopsMap);
router = new Router(timetable, stopsIndex);
});
it('should find a direct route', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop3')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const bestRoute = result.bestRoute();
assert.strictEqual(bestRoute?.legs.length, 1);
});
it('should return an empty result when no route is possible', () => {
const query = new Query.Builder()
.from('stop1')
.to('nonexistentStop')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const bestRoute = result.bestRoute();
assert.strictEqual(bestRoute, undefined);
});
it('should correctly calculate the arrival time to a stop', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop3')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const timeToStop3 = result.arrivalAt('stop3');
assert.strictEqual(
timeToStop3?.time.toMinutes(),
Time.fromString('08:35:00').toMinutes(),
);
});
});
describe('with a route change', () => {
let router: Router;
let timetable: Timetable;
beforeEach(() => {
const stopsAdjacency: StopsAdjacency = new Map([
[0, { transfers: [], routes: ['route1'] }],
[1, { transfers: [], routes: ['route1', 'route2'] }],
[2, { transfers: [], routes: ['route1'] }],
[3, { transfers: [], routes: ['route2'] }],
[4, { transfers: [], routes: ['route2'] }],
]);
const routesAdjacency: RoutesAdjacency = new Map([
[
'route1',
{
stopTimes: new Uint16Array([
Time.fromString('08:00:00').toMinutes(),
Time.fromString('08:15:00').toMinutes(),
Time.fromString('08:30:00').toMinutes(),
Time.fromString('08:45:00').toMinutes(),
Time.fromString('09:00:00').toMinutes(),
Time.fromString('09:10:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([0, 1, 2]),
stopIndices: new Map([
[0, 0],
[1, 1],
[2, 2],
]),
serviceRouteId: 'service_route1',
},
],
[
'route2',
{
stopTimes: new Uint16Array([
Time.fromString('08:05:00').toMinutes(),
Time.fromString('08:20:00').toMinutes(),
Time.fromString('09:00:00').toMinutes(),
Time.fromString('09:15:00').toMinutes(),
Time.fromString('09:20:00').toMinutes(),
Time.fromString('09:35:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([3, 1, 4]),
stopIndices: new Map([
[3, 0],
[1, 1],
[4, 2],
]),
serviceRouteId: 'service_route2',
},
],
]);
const routes: ServiceRoutesMap = new Map([
[
'service_route1',
{
type: 'BUS',
name: 'Line 1',
},
],
[
'service_route2',
{
type: 'RAIL',
name: 'Line 2',
},
],
]);
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
const stopsMap: StopsMap = new Map([
[
0,
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
lat: 1.0,
lon: 1.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
1,
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
lat: 2.0,
lon: 2.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
2,
{
id: 2,
sourceStopId: 'stop3',
name: 'Stop 3',
lat: 3.0,
lon: 3.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
3,
{
id: 3,
sourceStopId: 'stop4',
name: 'Stop 4',
lat: 4.0,
lon: 4.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
4,
{
id: 4,
sourceStopId: 'stop5',
name: 'Stop 5',
lat: 5.0,
lon: 5.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const stopsIndex = new StopsIndex(stopsMap);
router = new Router(timetable, stopsIndex);
});
it('should find a route with a change', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop5')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const bestRoute = result.bestRoute();
assert.strictEqual(bestRoute?.legs.length, 2);
});
it('should correctly calculate the arrival time to a stop', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop5')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const timeToStop5 = result.arrivalAt('stop5');
assert.strictEqual(
timeToStop5?.time.toMinutes(),
Time.fromString('09:20:00').toMinutes(),
);
});
});
describe('with a transfer', () => {
let router: Router;
let timetable: Timetable;
beforeEach(() => {
const stopsAdjacency: StopsAdjacency = new Map([
[0, { transfers: [], routes: ['route1'] }],
[
1,
{
transfers: [
{
destination: 4,
type: 'REQUIRES_MINIMAL_TIME',
minTransferTime: Duration.fromSeconds(300),
},
],
routes: ['route1'],
},
],
[2, { transfers: [], routes: ['route1'] }],
[3, { transfers: [], routes: ['route2'] }],
[4, { transfers: [], routes: ['route2'] }],
[5, { transfers: [], routes: ['route2'] }],
]);
const routesAdjacency: RoutesAdjacency = new Map([
[
'route1',
{
stopTimes: new Uint16Array([
Time.fromString('08:00:00').toMinutes(),
Time.fromString('08:15:00').toMinutes(),
Time.fromString('08:25:00').toMinutes(),
Time.fromString('08:35:00').toMinutes(),
Time.fromString('08:45:00').toMinutes(),
Time.fromString('08:55:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([0, 1, 2]),
stopIndices: new Map([
[0, 0],
[1, 1],
[2, 2],
]),
serviceRouteId: 'service_route1',
},
],
[
'route2',
{
stopTimes: new Uint16Array([
Time.fromString('08:10:00').toMinutes(),
Time.fromString('08:20:00').toMinutes(),
Time.fromString('08:40:00').toMinutes(),
Time.fromString('08:50:00').toMinutes(),
Time.fromString('09:00:00').toMinutes(),
Time.fromString('09:10:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([3, 4, 5]),
stopIndices: new Map([
[3, 0],
[4, 1],
[5, 2],
]),
serviceRouteId: 'service_route2',
},
],
]);
const routes: ServiceRoutesMap = new Map([
[
'service_route1',
{
type: 'BUS',
name: 'Line 1',
},
],
[
'service_route2',
{
type: 'RAIL',
name: 'Line 2',
},
],
]);
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
const stopsMap: StopsMap = new Map([
[
0,
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
lat: 1.0,
lon: 1.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
1,
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
lat: 2.0,
lon: 2.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
2,
{
id: 2,
sourceStopId: 'stop3',
name: 'Stop 3',
lat: 3.0,
lon: 3.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
3,
{
id: 3,
sourceStopId: 'stop4',
name: 'Stop 4',
lat: 4.0,
lon: 4.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
4,
{
id: 4,
sourceStopId: 'stop5',
name: 'Stop 5',
lat: 5.0,
lon: 5.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
5,
{
id: 5,
sourceStopId: 'stop6',
name: 'Stop 6',
lat: 6.0,
lon: 6.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const stopsIndex = new StopsIndex(stopsMap);
router = new Router(timetable, stopsIndex);
});
it('should find a route with a transfer', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop6')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const bestRoute = result.bestRoute();
assert.strictEqual(bestRoute?.legs.length, 3);
});
it('should correctly calculate the time to a stop', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop6')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const timeToStop5 = result.arrivalAt('stop5');
assert.strictEqual(
timeToStop5?.time.toMinutes(),
Time.fromString('08:30:00').toMinutes(),
);
});
});
describe('with a faster change', () => {
let router: Router;
let timetable: Timetable;
beforeEach(() => {
const stopsAdjacency: StopsAdjacency = new Map([
[0, { transfers: [], routes: ['route1'] }],
[1, { transfers: [], routes: ['route1', 'route2'] }],
[2, { transfers: [], routes: ['route1'] }],
[3, { transfers: [], routes: ['route2'] }],
[4, { transfers: [], routes: ['route2'] }],
]);
const routesAdjacency: RoutesAdjacency = new Map([
[
'route1',
{
stopTimes: new Uint16Array([
Time.fromString('08:00:00').toMinutes(),
Time.fromString('08:15:00').toMinutes(),
Time.fromString('08:30:00').toMinutes(),
Time.fromString('08:45:00').toMinutes(),
Time.fromString('09:00:00').toMinutes(),
Time.fromString('09:15:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([0, 1, 2]),
stopIndices: new Map([
[0, 0],
[1, 1],
[2, 2],
]),
serviceRouteId: 'service_route1',
},
],
[
'route2',
{
stopTimes: new Uint16Array([
Time.fromString('08:10:00').toMinutes(),
Time.fromString('08:25:00').toMinutes(),
Time.fromString('08:50:00').toMinutes(),
Time.fromString('09:05:00').toMinutes(),
Time.fromString('09:10:00').toMinutes(),
Time.fromString('09:25:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([3, 1, 4]),
stopIndices: new Map([
[3, 0],
[1, 1],
[4, 2],
]),
serviceRouteId: 'service_route2',
},
],
[
'route3',
{
stopTimes: new Uint16Array([
Time.fromString('08:00:00').toMinutes(),
Time.fromString('08:15:00').toMinutes(),
Time.fromString('09:45:00').toMinutes(),
Time.fromString('10:00:00').toMinutes(),
]),
pickUpDropOffTypes: new Uint8Array([
0, // REGULAR
0, // REGULAR
0, // REGULAR
0, // REGULAR
]),
stops: new Uint32Array([0, 4]),
stopIndices: new Map([
[0, 0],
[4, 1],
]),
serviceRouteId: 'service_route3',
},
],
]);
const routes: ServiceRoutesMap = new Map([
[
'service_route1',
{
type: 'BUS',
name: 'Line 1',
},
],
[
'service_route2',
{
type: 'RAIL',
name: 'Line 2',
},
],
[
'service_route3',
{
type: 'FERRY',
name: 'Line 3',
},
],
]);
timetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
const stopsMap: StopsMap = new Map([
[
0,
{
id: 0,
sourceStopId: 'stop1',
name: 'Stop 1',
lat: 1.0,
lon: 1.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
1,
{
id: 1,
sourceStopId: 'stop2',
name: 'Stop 2',
lat: 2.0,
lon: 2.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
2,
{
id: 2,
sourceStopId: 'stop3',
name: 'Stop 3',
lat: 3.0,
lon: 3.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
3,
{
id: 3,
sourceStopId: 'stop4',
name: 'Stop 4',
lat: 4.0,
lon: 4.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
[
4,
{
id: 4,
sourceStopId: 'stop5',
name: 'Stop 5',
lat: 5.0,
lon: 5.0,
children: [],
locationType: 'SIMPLE_STOP_OR_PLATFORM',
},
],
]);
const stopsIndex = new StopsIndex(stopsMap);
router = new Router(timetable, stopsIndex);
});
it('should find a faster route with a change', () => {
const query = new Query.Builder()
.from('stop1')
.to('stop5')
.departureTime(Time.fromString('08:00:00'))
.build();
const result: Result = router.route(query);
const bestRoute = result.bestRoute();
assert.strictEqual(bestRoute?.legs.length, 2);
});
});
});