UNPKG

minotor

Version:

A lightweight client-side transit routing library.

751 lines (708 loc) 20.3 kB
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); }); }); });