UNPKG

minotor

Version:

A lightweight client-side transit routing library.

205 lines (182 loc) 6.66 kB
import assert from 'node:assert'; import { describe, it } from 'node:test'; import { distance } from 'geokdbush'; import { Stop } from '../../stops/stops.js'; import { StopsIndex } from '../../stops/stopsIndex.js'; import { TransferTypes } from '../../timetable/timetable.js'; import { StraightLineTransferGenerator } from '../straightLineTransferGenerator.js'; // A tiny synthetic interchange: a subway and a tram stop ~100m apart, a bus // stop ~2km away, and a stop with no coordinates. const subwayLat = 59.357; const subwayLon = 18.102; const tramLat = 59.3576; const tramLon = 18.1035; const farLat = 59.37; const farLon = 18.13; const subway: Stop = { id: 0, name: 'Subway', lat: subwayLat, lon: subwayLon, children: [], locationType: 'SIMPLE_STOP_OR_PLATFORM', }; const tram: Stop = { id: 1, name: 'Tram', lat: tramLat, lon: tramLon, children: [], locationType: 'SIMPLE_STOP_OR_PLATFORM', }; const far: Stop = { id: 2, name: 'Far Away', lat: farLat, lon: farLon, children: [], locationType: 'SIMPLE_STOP_OR_PLATFORM', }; const noCoords: Stop = { id: 3, name: 'No Coordinates', children: [], locationType: 'SIMPLE_STOP_OR_PLATFORM', }; const stops = new StopsIndex([subway, tram, far, noCoords]); describe('StraightLineTransferGenerator', () => { it('connects nearby stops with typed walking transfers', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, }); const transfers = generator.generate([subway, tram], stops); const subwayToTram = (transfers.get(subway.id) ?? []).find( (t) => t.destination === tram.id, ); assert.ok(subwayToTram, 'expected a transfer from subway to tram'); assert.strictEqual(subwayToTram.type, TransferTypes.REQUIRES_MINIMAL_TIME); assert.ok( typeof subwayToTram.minTransferTime === 'number' && Number.isInteger(subwayToTram.minTransferTime), ); }); it('emits exactly one edge per direction when both stops are origins', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, }); const transfers = generator.generate([subway, tram], stops); const subwayToTram = (transfers.get(subway.id) ?? []).filter( (t) => t.destination === tram.id, ); const tramToSubway = (transfers.get(tram.id) ?? []).filter( (t) => t.destination === subway.id, ); assert.strictEqual(subwayToTram.length, 1); assert.strictEqual(tramToSubway.length, 1); }); it('only emits transfers from the given origins', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, }); // Only the subway is an origin; the reverse tram -> subway edge is only // emitted when the tram is itself visited as an origin. const transfers = generator.generate([subway], stops); const subwayToTram = (transfers.get(subway.id) ?? []).some( (t) => t.destination === tram.id, ); assert.ok(subwayToTram, 'expected subway -> tram'); assert.ok( !transfers.has(tram.id), 'expected no reverse edge when the tram is not an origin', ); }); it('does not connect stops beyond the radius', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, }); const transfers = generator.generate([subway], stops); const subwayToFar = (transfers.get(subway.id) ?? []).some( (t) => t.destination === far.id, ); assert.ok(!subwayToFar); assert.ok(!transfers.has(far.id)); }); it('connects farther stops when the radius is large enough', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 5000, }); const transfers = generator.generate([subway], stops); const subwayToFar = (transfers.get(subway.id) ?? []).some( (t) => t.destination === far.id, ); assert.ok(subwayToFar); }); it('ignores stops without coordinates', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 5000, }); const transfers = generator.generate([subway, noCoords], stops); assert.ok(!transfers.has(noCoords.id)); for (const stopTransfers of transfers.values()) { assert.ok(!stopTransfers.some((t) => t.destination === noCoords.id)); } }); it('derives the transfer time from distance, detour and walking speed', () => { const walkingSpeedKmh = 4.5; const detourFactor = 1.3; const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, walkingSpeedKmh, detourFactor, changePenaltyMinutes: 0, }); const transfers = generator.generate([subway], stops); const subwayToTram = (transfers.get(subway.id) ?? []).find( (t) => t.destination === tram.id, ); assert.ok(subwayToTram); const straightLineKm = distance(subwayLon, subwayLat, tramLon, tramLat); const expected = Math.round( ((straightLineKm * detourFactor) / walkingSpeedKmh) * 60, ); assert.strictEqual(subwayToTram.minTransferTime, expected); }); // Walking time between the subway and tram at the default speed and detour, // with the change penalty disabled, as a baseline the penalty test builds on. // Derived from the generator so it tracks the default speed/detour. const baseWalkingTime = (() => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, changePenaltyMinutes: 0, }); const transfer = generator .generate([subway], stops) .get(subway.id) ?.find((t) => t.destination === tram.id); assert.ok(transfer?.minTransferTime !== undefined); return transfer.minTransferTime; })(); it('adds a flat change penalty to every transfer', () => { const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, changePenaltyMinutes: 5, }); const transfers = generator.generate([subway], stops); const subwayToTram = (transfers.get(subway.id) ?? []).find( (t) => t.destination === tram.id, ); assert.ok(subwayToTram); assert.strictEqual(subwayToTram.minTransferTime, baseWalkingTime + 5); }); it('applies the default change penalty out of the box', () => { // Default changePenaltyMinutes is 3. const generator = new StraightLineTransferGenerator({ maxDistanceMeters: 500, }); const transfers = generator.generate([subway], stops); const subwayToTram = (transfers.get(subway.id) ?? []).find( (t) => t.destination === tram.id, ); assert.strictEqual(subwayToTram?.minTransferTime, baseWalkingTime + 3); }); });