UNPKG

minotor

Version:

A lightweight client-side transit routing library.

363 lines (327 loc) 11.2 kB
import assert from 'node:assert'; import { describe, it } from 'node:test'; import { Timetable } from '../../router.js'; import { Stop, StopId } from '../../stops/stops.js'; import { StopsIndex } from '../../stops/stopsIndex.js'; import { Route } from '../../timetable/route.js'; import { timeFromHM } from '../../timetable/time.js'; import { RouteTypes, ServiceRoute, StopAdjacency, } from '../../timetable/timetable.js'; import { ParetoRun, RangeResult } from '../rangeResult.js'; import { Result } from '../result.js'; import { RoutingState, VehicleEdge } from '../router.js'; // Two-stop timetable with two trips on a single route: // trip 0: stop 0 departs 09:00, stop 1 arrives 09:30 // trip 1: stop 0 departs 08:30, stop 1 arrives 09:10 const stops: Stop[] = [ { id: 0, sourceStopId: 'A', name: 'Stop A', lat: 0, lon: 0, children: [], locationType: 'SIMPLE_STOP_OR_PLATFORM', }, { id: 1, sourceStopId: 'B', name: 'Stop B', lat: 0, lon: 0, children: [], locationType: 'SIMPLE_STOP_OR_PLATFORM', }, ]; const stopsIndex = new StopsIndex(stops); const stopsAdjacency: StopAdjacency[] = [{ routes: [0] }, { routes: [0] }]; const routesAdjacency = [ Route.of({ id: 0, serviceRouteId: 0, trips: [ { stops: [ { id: 0, arrivalTime: timeFromHM(9, 0), departureTime: timeFromHM(9, 0), }, { id: 1, arrivalTime: timeFromHM(9, 30), departureTime: timeFromHM(9, 30), }, ], }, { stops: [ { id: 0, arrivalTime: timeFromHM(8, 30), departureTime: timeFromHM(8, 30), }, { id: 1, arrivalTime: timeFromHM(9, 10), departureTime: timeFromHM(9, 10), }, ], }, ], }), ]; const serviceRoutes: ServiceRoute[] = [ { type: RouteTypes.BUS, name: 'Line 1', routes: [0] }, ]; const timetable = new Timetable(stopsAdjacency, routesAdjacency, serviceRoutes); const DEST = 1; const DESTINATIONS = new Set([DEST]); // Run A — later departure (09:00→09:30, 30-minute duration) const edgeA: VehicleEdge = { arrival: timeFromHM(9, 30), stopIndex: 0, hopOffStopIndex: 1, routeId: 0, tripIndex: 0, }; const runA: ParetoRun = { departureTime: timeFromHM(9, 0), result: new Result( DESTINATIONS, RoutingState.fromTestData({ nbStops: 2, origins: [0], destinations: [DEST], arrivals: [ [0, timeFromHM(9, 0), 0], [DEST, timeFromHM(9, 30), 1], ], graph: [[[0, { stopId: 0, arrival: timeFromHM(9, 0) }]], [[DEST, edgeA]]], }), stopsIndex, timetable, ), }; // Run B — earlier departure (08:30→09:10, 40-minute duration) const edgeB: VehicleEdge = { arrival: timeFromHM(9, 10), stopIndex: 0, hopOffStopIndex: 1, routeId: 0, tripIndex: 1, }; const runB: ParetoRun = { departureTime: timeFromHM(8, 30), result: new Result( DESTINATIONS, RoutingState.fromTestData({ nbStops: 2, origins: [0], destinations: [DEST], arrivals: [ [0, timeFromHM(8, 30), 0], [DEST, timeFromHM(9, 10), 1], ], graph: [ [[0, { stopId: 0, arrival: timeFromHM(8, 30) }]], [[DEST, edgeB]], ], }), stopsIndex, timetable, ), }; // Runs are stored latest-departure-first: [runA, runB] const rangeResult = new RangeResult([runA, runB], DESTINATIONS); describe('RangeResult', () => { describe('size and destinations', () => { it('size reflects the number of Pareto runs', () => { assert.strictEqual(rangeResult.size, 2); }); it('destinations returns the resolved destination stop set', () => { assert.deepStrictEqual(rangeResult.destinations, DESTINATIONS); }); }); describe('[Symbol.iterator]', () => { it('iterates runs in latest-departure-first order', () => { const runs = [...rangeResult]; assert.strictEqual(runs.length, 2); assert.strictEqual(runs[0]?.departureTime, timeFromHM(9, 0)); assert.strictEqual(runs[1]?.departureTime, timeFromHM(8, 30)); }); }); describe('bestRoute', () => { it('returns the route that arrives earliest at the destination', () => { const route = rangeResult.bestRoute(); assert(route); assert.strictEqual(route.arrivalTime(), timeFromHM(9, 10)); assert.strictEqual(route.departureTime(), timeFromHM(8, 30)); }); it('accepts a specific stop ID', () => { const route = rangeResult.bestRoute(DEST); assert(route); assert.strictEqual(route.arrivalTime(), timeFromHM(9, 10)); }); it('returns undefined for an unreachable stop', () => { assert.strictEqual(rangeResult.bestRoute(99), undefined); }); }); describe('latestDepartureRoute', () => { it('returns the route with the latest possible departure', () => { const route = rangeResult.latestDepartureRoute(); assert(route); assert.strictEqual(route.departureTime(), timeFromHM(9, 0)); }); it('returns undefined for an unreachable stop', () => { assert.strictEqual(rangeResult.latestDepartureRoute(99), undefined); }); }); describe('fastestRoute', () => { it('returns the route with the shortest travel duration', () => { // runA: 30 min, runB: 40 min const route = rangeResult.fastestRoute(); assert(route); assert.strictEqual(route.departureTime(), timeFromHM(9, 0)); assert.strictEqual(route.totalDuration(), 30); }); it('returns undefined for an unreachable stop', () => { assert.strictEqual(rangeResult.fastestRoute(99), undefined); }); }); describe('getRoutes', () => { it('returns all routes ordered earliest-departure-first', () => { const routes = rangeResult.getRoutes(); assert.strictEqual(routes.length, 2); assert.strictEqual(routes[0]?.departureTime(), timeFromHM(8, 30)); assert.strictEqual(routes[1]?.departureTime(), timeFromHM(9, 0)); }); }); describe('earliestArrivalAt', () => { it('returns the earliest arrival across all Pareto runs', () => { const arrival = rangeResult.earliestArrivalAt(DEST); assert(arrival); assert.strictEqual(arrival.arrival, timeFromHM(9, 10)); }); it('returns undefined for a stop that was never reached', () => { assert.strictEqual(rangeResult.earliestArrivalAt(99), undefined); }); }); describe('shortestDurationTo', () => { it('returns the run with the minimum travel duration', () => { const result = rangeResult.shortestDurationTo(DEST); assert(result); assert.strictEqual(result.duration, 30); assert.strictEqual(result.arrival, timeFromHM(9, 30)); }); it('returns undefined for a stop that was never reached', () => { assert.strictEqual(rangeResult.shortestDurationTo(99), undefined); }); }); describe('allShortestDurations', () => { it('maps every reached stop to the shortest duration across all runs', () => { const durations = rangeResult.allShortestDurations(); assert.strictEqual(durations.get(0)?.duration, 0); assert.strictEqual(durations.get(DEST)?.duration, 30); }); }); describe('allEarliestArrivals', () => { it('maps every reached stop to the earliest arrival across all runs', () => { const arrivals = rangeResult.allEarliestArrivals(); assert.strictEqual(arrivals.get(0)?.arrival, timeFromHM(8, 30)); assert.strictEqual(arrivals.get(DEST)?.arrival, timeFromHM(9, 10)); }); }); describe('empty result', () => { it('handles an empty Pareto set gracefully', () => { const empty = new RangeResult([], DESTINATIONS); assert.strictEqual(empty.size, 0); assert.strictEqual(empty.bestRoute(), undefined); assert.strictEqual(empty.latestDepartureRoute(), undefined); assert.strictEqual(empty.fastestRoute(), undefined); assert.deepStrictEqual(empty.getRoutes(), []); assert.strictEqual(empty.earliestArrivalAt(DEST), undefined); assert.strictEqual(empty.shortestDurationTo(DEST), undefined); assert.deepStrictEqual(empty.allShortestDurations(), new Map()); assert.deepStrictEqual(empty.allEarliestArrivals(), new Map()); }); }); describe('no destinations (full-network / isochrone mode)', () => { // Same routing data as runA/runB but Result is built with empty destinations, // reflecting how RangeRouter constructs results in full-network mode. const emptyDests = new Set<StopId>(); const runAFull: ParetoRun = { departureTime: timeFromHM(9, 0), result: new Result( emptyDests, RoutingState.fromTestData({ nbStops: 2, origins: [0], destinations: [], arrivals: [ [0, timeFromHM(9, 0), 0], [DEST, timeFromHM(9, 30), 1], ], graph: [ [[0, { stopId: 0, arrival: timeFromHM(9, 0) }]], [[DEST, edgeA]], ], }), stopsIndex, timetable, ), }; const runBFull: ParetoRun = { departureTime: timeFromHM(8, 30), result: new Result( emptyDests, RoutingState.fromTestData({ nbStops: 2, origins: [0], destinations: [], arrivals: [ [0, timeFromHM(8, 30), 0], [DEST, timeFromHM(9, 10), 1], ], graph: [ [[0, { stopId: 0, arrival: timeFromHM(8, 30) }]], [[DEST, edgeB]], ], }), stopsIndex, timetable, ), }; const fullNetworkResult = new RangeResult([runAFull, runBFull], emptyDests); it('destinations is empty', () => { assert.strictEqual(fullNetworkResult.destinations.size, 0); }); it('getRoutes returns an empty array', () => { // Result was built with empty destinations, so bestRoute() on each inner // Result finds no target and getRoutes() collapses to []. assert.deepStrictEqual(fullNetworkResult.getRoutes(), []); }); it('bestRoute without an explicit stop returns undefined', () => { assert.strictEqual(fullNetworkResult.bestRoute(), undefined); }); it('bestRoute with an explicit stop returns the correct route', () => { const route = fullNetworkResult.bestRoute(DEST); assert(route); // Best route to DEST is via runB (earlier arrival at 09:10). assert.strictEqual(route.arrivalTime(), timeFromHM(9, 10)); }); it('allEarliestArrivals covers all reachable stops', () => { const arrivals = fullNetworkResult.allEarliestArrivals(); // DEST earliest arrival is 09:10 (from runB). assert.strictEqual(arrivals.get(DEST)?.arrival, timeFromHM(9, 10)); }); it('allShortestDurations covers all reachable stops', () => { const durations = fullNetworkResult.allShortestDurations(); // runA: 30 min, runB: 40 min → shortest duration to DEST is 30 min. assert.strictEqual(durations.get(DEST)?.duration, 30); }); }); });