diff --git a/.changeset/board-where-the-scan-boarded.md b/.changeset/board-where-the-scan-boarded.md new file mode 100644 index 0000000..00f4819 --- /dev/null +++ b/.changeset/board-where-the-scan-boarded.md @@ -0,0 +1,16 @@ +--- +"connection-scan-algorithm": patch +--- + +Return the journey the scan made. A journey could change trains at a station where another trip +happened to arrive first, with less than the station's interchange time, when the passenger had +really boarded that trip at an earlier call. The arrival times were right, the legs were not. + +The scan now records the connection each trip is boarded from, and the connection index holds that +connection for each station rather than the last connection into it, so each entry is one leg. A trip +that can be boarded at more than one call is boarded where the passenger has taken the fewest legs to +reach, then at the latest call, so a passenger is not taken past a call of the trip only to ride back +through it. Reaching a station at the same time in fewer legs now replaces how it was reached, and +the footpaths from it are walked again. Where the trip before still passes a later call of the next +one in time to change there, the journey changes there. `ScanResults.setConnection` has to follow +`isReachable` for the same connection, as the scan does. diff --git a/src/csa/ConnectionScanAlgorithm.ts b/src/csa/ConnectionScanAlgorithm.ts index 9cb8cca..2052bbb 100644 --- a/src/csa/ConnectionScanAlgorithm.ts +++ b/src/csa/ConnectionScanAlgorithm.ts @@ -55,9 +55,10 @@ export class ConnectionScanAlgorithm { } /** - * Walk every footpath out of a station whenever it is reached earlier than it was, not only the - * first time: a station first reached on foot is often then reached sooner by train, and the - * footpaths onwards from it have to start from the earlier time. + * Walk every footpath out of a station whenever it is reached earlier or in fewer legs than it + * was, not only the first time: a station first reached on foot is often then reached sooner by + * train, and the footpaths onwards from it have to start from the earlier time and count on from + * the fewer legs. */ private scanTransfers(results: ScanResults, origin: StopIdx): void { for (let t = this.transfers.offsets[origin]; t < this.transfers.offsets[origin + 1]; t++) { @@ -71,8 +72,8 @@ export class ConnectionScanAlgorithm { } /** - * The connection or footpath that achieves the earliest arrival at each station, by station index, - * or NO_CONNECTION where nothing does. + * How the earliest arrival at each station was made, by station index: the connection its trip was + * boarded from, or the footpath, or NO_CONNECTION where nothing reaches it. Each is one leg. */ export type ConnectionIndex = Int32Array; diff --git a/src/csa/ScanResults.ts b/src/csa/ScanResults.ts index e7fde04..ad0887b 100644 --- a/src/csa/ScanResults.ts +++ b/src/csa/ScanResults.ts @@ -14,6 +14,9 @@ export const NOT_REACHED = 0x7fffffff; /** The trip has carried the passenger to none of its calls */ export const NOT_CARRIED = 0x7fffffff; +/** More calls than any trip has, so that one fewer leg outranks any later call */ +const CALLS_PER_LEG = 0x10000; + /** * Mutable object that stores the current earliest arrival and best connection indexes as the * connections are being scanned. @@ -27,6 +30,7 @@ export class ScanResults { private readonly interchange: Int32Array; private readonly earliestArrivals: Int32Array; private readonly connectionIndex: ConnectionIndex; + private readonly legs: Int32Array; private readonly origins: StopIdx[] = []; private readonly destinations: StopIdx[] = []; private readonly isDestination: Uint8Array; @@ -34,19 +38,24 @@ export class ScanResults { /** * The trip arrivals are the earliest call each trip has carried the passenger to. They are only - * needed while the scan runs, so the factory gives every scan the same array. + * needed while the scan runs, so the factory gives every scan the same array. So are the trip + * boardings, the connection each trip is boarded from, and their ranks. Those are only read for a + * trip once it has carried the passenger, so need no clearing between scans. */ constructor( gtfs: GtfsData, origins: OriginDepartureTimes, destinations: StopID[], - private readonly tripArrivals: Int32Array + private readonly tripArrivals: Int32Array, + private readonly tripBoardings: Int32Array, + private readonly tripBoardingRanks: Int32Array ) { this.connections = gtfs.connections; this.transfers = gtfs.transfers; this.interchange = gtfs.interchange; this.earliestArrivals = new Int32Array(gtfs.stopTable.size).fill(NOT_REACHED); this.connectionIndex = new Int32Array(gtfs.stopTable.size).fill(NO_CONNECTION); + this.legs = new Int32Array(gtfs.stopTable.size); this.isDestination = new Uint8Array(gtfs.stopTable.size); for (const code of Object.keys(origins)) { @@ -75,17 +84,31 @@ export class ScanResults { * Once a trip has carried the passenger to a call, they are still aboard for any of its * connections from there on. Boarding it is not the same: a trip only picking up at a later call * has carried nobody to it. + * + * A trip that can be boarded at more than one call is boarded where the passenger has taken the + * fewest legs to reach, and after that at the latest call. Boarding at the earliest call would + * have a passenger who passed a later call of the trip on the way ride back through it. Both are + * folded into one rank, lower being better, and kept per trip so that comparing against the + * current boarding reads nothing from the connections. */ public isReachable(c: Connection): boolean { - const reachable = this.isReachableWithChange(c) || this.isReachableFromSameService(c); + const trip = this.connections.trip[c]; - if (reachable) { - const trip = this.connections.trip[c]; + if (this.isReachableWithChange(c)) { + const rank = this.legs[this.connections.departureStation[c]] * CALLS_PER_LEG - this.connections.board[c]; - this.tripArrivals[trip] = Math.min(this.tripArrivals[trip], this.connections.alight[c]); + if (this.tripArrivals[trip] === NOT_CARRIED || rank < this.tripBoardingRanks[trip]) { + this.tripBoardings[trip] = c; + this.tripBoardingRanks[trip] = rank; + } } + else if (!this.isReachableFromSameService(c)) { + return false; + } + + this.tripArrivals[trip] = Math.min(this.tripArrivals[trip], this.connections.alight[c]); - return reachable; + return true; } private isReachableFromSameService(c: Connection): boolean { @@ -99,17 +122,27 @@ export class ScanResults { return this.earliestArrivals[origin] + interchange <= this.connections.departureTime[c]; } + /** + * Arriving at the same time is better in fewer legs, as the stations reached from here and the + * trips boarded here count their legs from it. + */ public isBetter(c: Connection): boolean { - const arrivalTime = this.earliestArrivals[this.connections.arrivalStation[c]]; + const destination = this.connections.arrivalStation[c]; + const arrivalTime = this.earliestArrivals[destination]; - return arrivalTime > this.connections.arrivalTime[c] - || (arrivalTime === this.connections.arrivalTime[c] && this.staysAboard(c)); + if (arrivalTime !== this.connections.arrivalTime[c]) { + return arrivalTime > this.connections.arrivalTime[c]; + } + + const legs = this.legsTo(c); + + return legs < this.legs[destination] || (legs === this.legs[destination] && this.staysAboard(c)); } /** - * Arriving at the same time without changing is better than arriving on another trip. A vehicle - * that couples onto another runs as a trip of its own alongside both portions, so without this - * whichever of them was scanned first would have the passenger change at the coupling. + * Arriving at the same time in as many legs without changing is better than arriving on another + * trip. A vehicle that couples onto another runs as a trip of its own alongside both portions, so + * without this whichever of them was scanned first would have the passenger change at the coupling. */ private staysAboard(c: Connection): boolean { const current = this.connectionIndex[this.connections.arrivalStation[c]]; @@ -120,26 +153,37 @@ export class ScanResults { } /** - * Returns true if the connection arrives earlier than the destination was reached before, rather - * than at the same time on a trip the passenger stays aboard + * Returns true if the connection reaches the destination earlier or in fewer legs than before, + * rather than at the same time in as many on a trip the passenger stays aboard */ public setConnection(c: Connection): boolean { const destination = this.connections.arrivalStation[c]; const previous = this.earliestArrivals[destination]; + const previousLegs = this.legs[destination]; - this.connectionIndex[destination] = c; + this.connectionIndex[destination] = this.tripBoardings[this.connections.trip[c]]; + this.legs[destination] = this.legsTo(c); - return this.arrive(destination, this.connections.arrivalTime[c]) < previous; + return this.arrive(destination, this.connections.arrivalTime[c]) < previous || this.legs[destination] < previousLegs; + } + + private legsTo(c: Connection): number { + return this.legs[this.connections.departureStation[this.tripBoardings[this.connections.trip[c]]]] + 1; } public isTransferBetter(t: number): boolean { - return this.earliestArrivals[this.transfers.destination[t]] > this.getTransferArrivalTime(t); + const destination = this.transfers.destination[t]; + const arrivalTime = this.getTransferArrivalTime(t); + + return this.earliestArrivals[destination] > arrivalTime + || (this.earliestArrivals[destination] === arrivalTime && this.legs[this.transfers.origin[t]] + 1 < this.legs[destination]); } public setTransfer(t: number): void { const destination = this.transfers.destination[t]; this.connectionIndex[destination] = transferConnection(t); + this.legs[destination] = this.legs[this.transfers.origin[t]] + 1; this.arrive(destination, this.getTransferArrivalTime(t)); } diff --git a/src/csa/ScanResultsFactory.ts b/src/csa/ScanResultsFactory.ts index 130aa39..1d0cfbb 100644 --- a/src/csa/ScanResultsFactory.ts +++ b/src/csa/ScanResultsFactory.ts @@ -8,11 +8,15 @@ import { NOT_CARRIED, ScanResults } from "./ScanResults.js"; */ export class ScanResultsFactory { private readonly tripArrivals: Int32Array; + private readonly tripBoardings: Int32Array; + private readonly tripBoardingRanks: Int32Array; constructor( private readonly gtfs: GtfsData ) { this.tripArrivals = new Int32Array(gtfs.trips.length); + this.tripBoardings = new Int32Array(gtfs.trips.length); + this.tripBoardingRanks = new Int32Array(gtfs.trips.length); } /** @@ -20,7 +24,7 @@ export class ScanResultsFactory { * of the scan in progress can still be asked whether a connection is reachable. */ public create(origins: OriginDepartureTimes, destinations: StopID[]): ScanResults { - return new ScanResults(this.gtfs, origins, destinations, this.tripArrivals.fill(NOT_CARRIED)); + return new ScanResults(this.gtfs, origins, destinations, this.tripArrivals.fill(NOT_CARRIED), this.tripBoardings, this.tripBoardingRanks); } } diff --git a/src/journey/JourneyFactory.ts b/src/journey/JourneyFactory.ts index e3856bf..b798cbe 100644 --- a/src/journey/JourneyFactory.ts +++ b/src/journey/JourneyFactory.ts @@ -1,9 +1,9 @@ import { isCall, type StopID, type StopTime, type Time, type Trip } from "@gb-transit/gtfs-loader"; import type { ConnectionIndex } from "../csa/ConnectionScanAlgorithm.js"; import type { GtfsData } from "../gtfs/GtfsLoader.js"; -import { UNKNOWN_STOP } from "../gtfs/StopTable.js"; -import { type Connection, isChangeRequired, isTransferConnection, NO_CONNECTION, transferOf } from "./Connection.js"; -import { type AnyLeg, isTransfer, type Journey } from "./Journey.js"; +import { type StopIdx, UNKNOWN_STOP } from "../gtfs/StopTable.js"; +import { type Connection, isTransferConnection, NO_CONNECTION, transferOf } from "./Connection.js"; +import { type AnyLeg, isTransfer, type Journey, type TimetableLeg } from "./Journey.js"; /** * Creates journeys from the connection index created by the connection scan algorithm. @@ -26,53 +26,44 @@ export class JourneyFactory { .map(d => this.getLegs(connections, d)) .filter((c): c is AnyLeg[] => c !== null) .map(c => this.getCompactedLegs(c)) + .map(c => this.getStraightenedLegs(c)) .map(l => this.getJourney(l)); } /** - * Iterate backwards from the destination to the origin collecting connections into legs + * Iterate backwards from the destination to the origin, each station giving the leg that reached it */ private getLegs(connections: ConnectionIndex, destination: StopID): AnyLeg[] | null { - const legs: Connection[][] = []; - let legConnections: Connection[] = []; - let previousConnection: Connection = NO_CONNECTION; + const legs: AnyLeg[] = []; let station = this.gtfs.stopTable.indexOf(destination); while (station !== UNKNOWN_STOP && connections[station] !== NO_CONNECTION) { const connection = connections[station]; - if (previousConnection !== NO_CONNECTION && isChangeRequired(this.gtfs.connections, previousConnection, connection)) { - legs.push(legConnections.reverse()); - legConnections = []; - } - - legConnections.push(connection); - previousConnection = connection; + legs.push(this.toLeg(connection, station)); station = isTransferConnection(connection) ? this.gtfs.transfers.origin[transferOf(connection)] : this.gtfs.connections.departureStation[connection]; } - legs.push(legConnections.reverse()); - - return legConnections.length === 0 ? null : legs.reverse().map(cs => this.toLeg(cs)); + return legs.length === 0 ? null : legs.reverse(); } /** - * Convert a list of connections into a Transfer or a TimetableLeg + * Convert the connection a trip was boarded from into a TimetableLeg to the station, or a footpath + * into a Transfer */ - private toLeg(cs: Connection[]): AnyLeg { + private toLeg(connection: Connection, station: StopIdx): AnyLeg { const { connections, stopTable, transfers, trips } = this.gtfs; - const firstConnection = cs[0]; - if (isTransferConnection(firstConnection)) { - return transfers.transfer[transferOf(firstConnection)]; + if (isTransferConnection(connection)) { + return transfers.transfer[transferOf(connection)]; } else { - const origin = stopTable.nameOf(connections.departureStation[firstConnection]); - const destination = stopTable.nameOf(connections.arrivalStation[cs[cs.length - 1]]); - const trip = trips[connections.trip[firstConnection]]; - const stopTimes = this.getStopTimes(trip, origin, connections.departureTime[firstConnection], destination); + const origin = stopTable.nameOf(connections.departureStation[connection]); + const destination = stopTable.nameOf(station); + const trip = trips[connections.trip[connection]]; + const stopTimes = this.getStopTimes(trip, origin, connections.departureTime[connection], destination); return { origin, destination, trip, stopTimes: stopTimes || [] }; } @@ -112,6 +103,54 @@ export class JourneyFactory { return newLegs.reverse(); } + /** + * The scan boards a trip at the call reached in the fewest legs, but only knows the legs of the + * earliest arrival at each call. The trip before may pass a later call of the next one after that + * earliest arrival, leaving the passenger riding on to where they board and back through it. Where + * there is time to change at such a call, the passenger changes there instead. + */ + private getStraightenedLegs(legs: AnyLeg[]): AnyLeg[] { + for (let i = 1; i < legs.length; i++) { + const previous = legs[i - 1]; + const next = legs[i]; + + if (!isTransfer(previous) && !isTransfer(next)) { + this.changeAtFirstSharedCall(previous, next); + } + } + + return legs; + } + + /** + * Cut the previous leg at the first call it sets down at that the next leg goes on to pick up at in + * time, and board the next leg there, so the whole of the ride on and back is left out. + */ + private changeAtFirstSharedCall(previous: TimetableLeg, next: TimetableLeg): void { + for (let i = 1; i < previous.stopTimes.length - 1; i++) { + const alight = previous.stopTimes[i]; + + if (!alight.dropOff) { + continue; + } + + const station = this.stationOf(alight); + const departureTime = alight.arrivalTime + (this.gtfs.interchange[this.gtfs.stopTable.indexOf(station)] ?? 0); + const board = next.stopTimes.findIndex((c, j) => + j > 0 && j < next.stopTimes.length - 1 && c.pickUp && c.departureTime >= departureTime && this.stationOf(c) === station + ); + + if (board !== -1) { + previous.stopTimes = previous.stopTimes.slice(0, i + 1); + previous.destination = station; + next.stopTimes = next.stopTimes.slice(board); + next.origin = station; + + return; + } + } + } + /** * Try to create a new leg from the trip, ensuring the new leg departs the origin no earlier than * the given departure time. The stop times are the feed's own, so a leg between two stations still diff --git a/test/unit/csa/ConnectionScanAlgorithm.spec.ts b/test/unit/csa/ConnectionScanAlgorithm.spec.ts index 143e184..2db7f5f 100644 --- a/test/unit/csa/ConnectionScanAlgorithm.spec.ts +++ b/test/unit/csa/ConnectionScanAlgorithm.spec.ts @@ -129,6 +129,94 @@ describe("ConnectionScanAlgorithm", () => { expect(legsOf(journey)).toEqual(["1:A-D"]); }); + /** + * Trip 2 reaches D before trip 3 does, but too late to change onto trip 3 there. The passenger + * boards trip 3 at B instead, and the journey has to say so rather than change at D. + */ + it("boards a trip where the scan boarded it rather than where another trip arrived first", () => { + const [journey] = plan({ + trips: [ + trip("1", [st("A", 1000), st("B", 1100)]), + trip("2", [st("A", 1050), st("D", 1125)]), + trip("3", [st("B", 1110), st("D", 1130), st("E", 1150)]) + ], + interchange: { D: 600 } + }, ["A"], ["E"], 0); + + expect(legsOf(journey)).toEqual(["1:A-B", "3:B-E"]); + expect(journey.arrivalTime).toBe(1150); + }); + + /** + * Trip 1 passes Q on its way to P, where trip 2 starts back through Q. Trip 2 could be boarded at + * either, but boarding it at P would ride from Q to P and straight back. + */ + it("boards a trip at its latest call reached in as few legs rather than doubling back", () => { + const [journey] = plan({ + trips: [ + trip("1", [st("X", 1000), st("Q", 1010), st("P", 1020)]), + trip("2", [st("P", 1030), st("Q", 1040), st("R", 1100)]) + ] + }, ["X"], ["R"], 900); + + expect(legsOf(journey)).toEqual(["1:X-Q", "2:Q-R"]); + expect(journey.arrivalTime).toBe(1100); + }); + + /** + * Trip 2 overtakes trip 1 and could be changed from at B, but that is a change trip 1 does not need. + */ + it("does not board a trip later if reaching the later call takes more legs", () => { + const [journey] = plan({ + trips: [ + trip("1", [st("A", 1000), st("B", 1100), st("C", 1200)]), + trip("2", [st("A", 1010), st("B", 1050)]) + ] + }, ["A"], ["C"], 900); + + expect(legsOf(journey)).toEqual(["1:A-C"]); + }); + + /** + * S is reached at the same time in three legs, then in one on trip c, and W is walked to from S. + * Trip d can be boarded at F, reached in three legs, or at W, which is only in two once the walk + * from S is taken again after trip c. + */ + it("walks on again from a station reached at the same time in fewer legs", () => { + const [journey] = plan({ + trips: [ + trip("a", [st("O", 1000), st("M", 1010)]), + trip("a2", [st("M", 1020), st("N", 1030)]), + trip("b", [st("N", 1040), st("S", 1100)]), + trip("e", [st("N", 1040), st("F", 1100)]), + trip("c", [st("O", 1000), st("S", 1100)]), + trip("d", [st("F", 1150), st("W", 1200), st("T", 1300)]) + ], + transfers: byOrigin(walk("S", "W", 60)) + }, ["O"], ["T"], 900); + + expect(legsOf(journey)).toEqual(["c:O-S", "walk:S-W", "d:W-T"]); + expect(journey.arrivalTime).toBe(1300); + }); + + /** + * X is reached earliest in two legs, but trip A passes it later in one on the way to Y, where trip + * B is boarded back through X. The passenger changes at X rather than riding to Y and back. + */ + it("changes where the previous trip passed the next rather than riding on and back", () => { + const [journey] = plan({ + trips: [ + trip("P", [st("O", 1000), st("Q", 1005)]), + trip("R", [st("Q", 1006), st("X", 1010)]), + trip("A", [st("O", 1000), st("X", 1020), st("Y", 1030)]), + trip("B", [st("Y", 1040), st("X", 1050), st("Z", 1100)]) + ] + }, ["O"], ["Z"], 900); + + expect(legsOf(journey)).toEqual(["A:O-X", "B:X-Z"]); + expect(journey.arrivalTime).toBe(1100); + }); + it("gives each scan a connection index of its own", () => { const gtfs = gtfsOf({ trips: [trip("1", [st("A", 1000), st("B", 1100), st("C", 1200)])] }); const csa = new ConnectionScanAlgorithm(gtfs, new ScanResultsFactory(gtfs)); diff --git a/test/unit/csa/ScanResults.spec.ts b/test/unit/csa/ScanResults.spec.ts index be6fd07..50be054 100644 --- a/test/unit/csa/ScanResults.spec.ts +++ b/test/unit/csa/ScanResults.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { ScanResults } from "../../../src/csa/ScanResults.js"; import { ScanResultsFactory } from "../../../src/csa/ScanResultsFactory.js"; import { transferConnection } from "../../../src/journey/Connection.js"; -import { byOrigin, connection, gtfsOf, pickUpOnly, resultsFor, st, transfer, trip, walk } from "../util.js"; +import { byOrigin, connection, gtfsOf, pickUpOnly, resultsFor, st, take, transfer, trip, walk } from "../util.js"; describe("ScanResults", () => { const gtfs = gtfsOf({ @@ -97,8 +97,8 @@ describe("ScanResults", () => { }); const results = resultsFor(parallel, { A: 900 }); - results.setConnection(connection(parallel, "1", "A", "B")); - results.setConnection(connection(parallel, "2", "B", "C")); + take(results, connection(parallel, "1", "A", "B")); + take(results, connection(parallel, "2", "B", "C")); const alsoToC = connection(parallel, "3", "B", "C"); @@ -106,6 +106,41 @@ describe("ScanResults", () => { expect(results.isBetter(alsoToC)).toBe(false); }); + it("replaces a trip with another arriving at the same time in fewer legs", () => { + const shorter = gtfsOf({ + trips: [ + trip("1", [st("A", 1000), st("B", 1010)]), + trip("2", [st("B", 1020), st("C", 1100)]), + trip("3", [st("A", 1000), st("C", 1100)]) + ] + }); + const results = resultsFor(shorter, { A: 900 }); + + take(results, connection(shorter, "1", "A", "B")); + + expect(results.isReachable(connection(shorter, "2", "B", "C"))).toBe(true); + expect(results.setConnection(connection(shorter, "2", "B", "C"))).toBe(true); + + const direct = connection(shorter, "3", "A", "C"); + + expect(results.isReachable(direct)).toBe(true); + expect(results.isBetter(direct)).toBe(true); + expect(results.setConnection(direct)).toBe(true); + }); + + it("knows a transfer arriving at the same time in fewer legs is better", () => { + const walks = gtfsOf({ + trips: [trip("1", [st("A", 1000), st("B", 1010)]), trip("2", [st("B", 1020), st("C", 1100)])], + transfers: byOrigin(walk("A", "C", 100)) + }); + const results = resultsFor(walks, { A: 1000 }); + + take(results, connection(walks, "1", "A", "B")); + take(results, connection(walks, "2", "B", "C")); + + expect(results.isTransferBetter(transfer(walks, "A", "C"))).toBe(true); + }); + it("does not count a passenger aboard at a call the trip only picks up at", () => { const pickUps = gtfsOf({ trips: [trip("1", [pickUpOnly("A", 1000), pickUpOnly("B", 1060), st("C", 2000)])] }); const results = resultsFor(pickUps, { A: 900 }); @@ -153,13 +188,23 @@ describe("ScanResults", () => { const results = resultsFor(gtfs, { A: 900 }); const [b, c] = ["B", "C"].map(code => gtfs.stopTable.indexOf(code)); - results.setConnection(connection(gtfs, "LN1111", "A", "B")); + take(results, connection(gtfs, "LN1111", "A", "B")); results.setTransfer(transfer(gtfs, "B", "C")); expect(results.getConnectionIndex()[b]).toBe(connection(gtfs, "LN1111", "A", "B")); expect(results.getConnectionIndex()[c]).toBe(transferConnection(transfer(gtfs, "B", "C"))); }); + it("indexes a station by the connection its trip was boarded from", () => { + const through = gtfsOf({ trips: [trip("1", [st("A", 1000), st("B", 1015), st("C", 1030)])] }); + const results = resultsFor(through, { A: 900 }); + + take(results, connection(through, "1", "A", "B")); + take(results, connection(through, "1", "B", "C")); + + expect(results.getConnectionIndex()[through.stopTable.indexOf("C")]).toBe(connection(through, "1", "A", "B")); + }); + it("is finished once a connection arrives after every destination was reached", () => { const results = resultsFor(gtfs, { A: 900 }, ["B"]); diff --git a/test/unit/journey/JourneyFactory.spec.ts b/test/unit/journey/JourneyFactory.spec.ts index 1de7027..a528350 100644 --- a/test/unit/journey/JourneyFactory.spec.ts +++ b/test/unit/journey/JourneyFactory.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import type { TimetableLeg } from "../../../src/journey/Journey.js"; import { JourneyFactory } from "../../../src/journey/JourneyFactory.js"; -import { byOrigin, connection, gtfsOf, legsOf, platforms, resultsFor, st, transfer, trip, walk } from "../util.js"; +import { byOrigin, connection, gtfsOf, legsOf, platforms, resultsFor, st, take, transfer, trip, walk } from "../util.js"; describe("JourneyFactory", () => { @@ -9,7 +9,7 @@ describe("JourneyFactory", () => { const gtfs = gtfsOf({ trips: [trip("1", [st("A", 1000), st("B", 1030)])] }); const results = resultsFor(gtfs, { A: 1000 }); - results.setConnection(connection(gtfs, "1", "A", "B")); + take(results, connection(gtfs, "1", "A", "B")); const [journey] = new JourneyFactory(gtfs).getJourneys(results.getConnectionIndex(), ["B"]); @@ -34,7 +34,7 @@ describe("JourneyFactory", () => { const results = resultsFor(gtfs, { A: 1000 }); results.setTransfer(transfer(gtfs, "A", "B")); - results.setConnection(connection(gtfs, "1", "B", "C")); + take(results, connection(gtfs, "1", "B", "C")); const [journey] = new JourneyFactory(gtfs).getJourneys(results.getConnectionIndex(), ["C"]); @@ -51,7 +51,7 @@ describe("JourneyFactory", () => { const results = resultsFor(gtfs, { A: 1000 }); results.setTransfer(transfer(gtfs, "A", "B")); - results.setConnection(connection(gtfs, "1", "B", "C")); + take(results, connection(gtfs, "1", "B", "C")); results.setTransfer(transfer(gtfs, "C", "D")); const [journey] = new JourneyFactory(gtfs).getJourneys(results.getConnectionIndex(), ["D"]); @@ -68,10 +68,10 @@ describe("JourneyFactory", () => { }); const results = resultsFor(gtfs, { A: 1000 }); - results.setConnection(connection(gtfs, "LN1111", "A", "B")); - results.setConnection(connection(gtfs, "LN1112", "B", "C")); - results.setConnection(connection(gtfs, "LN1113", "C", "D")); - results.setConnection(connection(gtfs, "LN1114", "D", "E")); + take(results, connection(gtfs, "LN1111", "A", "B")); + take(results, connection(gtfs, "LN1112", "B", "C")); + take(results, connection(gtfs, "LN1113", "C", "D")); + take(results, connection(gtfs, "LN1114", "D", "E")); const [journey] = new JourneyFactory(gtfs).getJourneys(results.getConnectionIndex(), ["E"]); @@ -85,7 +85,7 @@ describe("JourneyFactory", () => { const gtfs = gtfsOf({ stops: platforms, trips: [trip("1", [st("NRW1", 1000), passing, st("LST8", 1200)])] }); const results = resultsFor(gtfs, { NRW: 900 }); - results.setConnection(connection(gtfs, "1", "NRW", "LST")); + take(results, connection(gtfs, "1", "NRW", "LST")); const [journey] = new JourneyFactory(gtfs).getJourneys(results.getConnectionIndex(), ["LST"]); const leg = journey.legs[0] as TimetableLeg; diff --git a/test/unit/util.ts b/test/unit/util.ts index 2775922..3a2b0e1 100644 --- a/test/unit/util.ts +++ b/test/unit/util.ts @@ -146,6 +146,17 @@ export function resultsFor(gtfs: GtfsData, origins: Record, destin return new ScanResultsFactory(gtfs).create(origins, destinations); } +/** + * Take a connection as the scan does, which is only once it has been found reachable. + */ +export function take(results: ScanResults, c: Connection): void { + if (!results.isReachable(c)) { + throw new Error(`Connection ${c} is not reachable`); + } + + results.setConnection(c); +} + /** * Plan over a feed with the pieces a caller would wire together. */