minotor
Version:
A lightweight client-side transit routing library.
266 lines (254 loc) • 10.1 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { StopId } from '../stops/stops.js';
import { StopsIndex } from '../stops/stopsIndex.js';
import { Duration } from '../timetable/duration.js';
import { Time } from '../timetable/time.js';
import { NOT_AVAILABLE, Timetable } from '../timetable/timetable.js';
import { Query } from './query.js';
import { Result } from './result.js';
import { Leg } from './route.js';
const UNREACHED = Time.infinity();
export type TripLeg = ReachingTime & {
leg?: Leg; // leg is not set for the very first segment
};
export type ReachingTime = {
time: Time;
legNumber: number;
origin: StopId;
};
type CurrentTrip = {
trip: number;
origin: StopId;
bestHopOnStop: StopId;
};
export class Router {
private readonly timetable: Timetable;
private readonly stopsIndex: StopsIndex;
constructor(timetable: Timetable, stopsIndex: StopsIndex) {
this.timetable = timetable;
this.stopsIndex = stopsIndex;
}
/**
* Evaluates possible transfers for a given query on a transport
* network, updating the earliest arrivals at various stops and marking new
* stops that can be reached through these transfers.
*/
private considerTransfers(
query: Query,
markedStops: Set<StopId>,
arrivalsAtCurrentRound: Map<StopId, TripLeg>,
earliestArrivals: Map<StopId, ReachingTime>,
round: number,
): void {
const { options } = query;
const newlyMarkedStops: Set<StopId> = new Set();
for (const stop of markedStops) {
for (const transfer of this.timetable.getTransfers(stop)) {
let transferTime: Duration;
if (transfer.minTransferTime) {
transferTime = transfer.minTransferTime;
} else if (transfer.type === 'IN_SEAT') {
transferTime = Duration.zero();
} else {
transferTime = options.minTransferTime;
}
const arrivalAfterTransfer = arrivalsAtCurrentRound
.get(stop)!
.time.plus(transferTime);
const originalArrival =
arrivalsAtCurrentRound.get(transfer.destination)?.time ?? UNREACHED;
if (arrivalAfterTransfer.toMinutes() < originalArrival.toMinutes()) {
const origin = arrivalsAtCurrentRound.get(stop)?.origin ?? stop;
arrivalsAtCurrentRound.set(transfer.destination, {
time: arrivalAfterTransfer,
legNumber: round,
origin: origin,
leg: {
from: this.stopsIndex.findStopById(stop)!,
to: this.stopsIndex.findStopById(transfer.destination)!,
minTransferTime: transfer.minTransferTime,
type: transfer.type,
},
});
earliestArrivals.set(transfer.destination, {
time: arrivalAfterTransfer,
legNumber: round,
origin: origin,
});
newlyMarkedStops.add(transfer.destination);
}
}
}
for (const newStop of newlyMarkedStops) {
markedStops.add(newStop);
}
}
/**
* The main Raptor algorithm implementation.
*
* @param query The query containing the main parameters for the routing.
* @returns A result object containing data structures allowing to reconstruct routes and .
*/
route(query: Query): Result {
const { from, to, departureTime, options } = query;
// Consider children or siblings of the "from" stop as potential origins
const origins = this.stopsIndex.equivalentStops(from);
// Consider children or siblings of the "to" stop(s) as potential destinations
const destinations = to.flatMap((destination) =>
this.stopsIndex.equivalentStops(destination),
);
const earliestArrivals = new Map<StopId, ReachingTime>();
const earliestArrivalsWithoutAnyLeg = new Map<StopId, TripLeg>();
const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
// Stops that have been improved at round k-1
const markedStops = new Set<StopId>();
for (const originStop of origins) {
markedStops.add(originStop.id);
earliestArrivals.set(originStop.id, {
time: departureTime,
legNumber: 0,
origin: originStop.id,
});
earliestArrivalsWithoutAnyLeg.set(originStop.id, {
time: departureTime,
legNumber: 0,
origin: originStop.id,
});
}
// on the first round we need to first consider transfers to discover all possible route origins
this.considerTransfers(
query,
markedStops,
earliestArrivalsWithoutAnyLeg,
earliestArrivals,
0,
);
for (let round = 1; round <= options.maxTransfers + 1; round++) {
const arrivalsAtCurrentRound = new Map<StopId, TripLeg>();
earliestArrivalsPerRound.push(arrivalsAtCurrentRound);
const arrivalsAtPreviousRound = earliestArrivalsPerRound[round - 1]!;
// Routes that contain at least one stop reached with at least round - 1 legs
// together with corresponding hop on stop index (earliest marked stop)
const reachableRoutes = this.timetable.findReachableRoutes(
markedStops,
options.transportModes,
);
markedStops.clear();
// for each route that can be reached with at least round - 1 trips
for (const [routeId, hopOnStop] of reachableRoutes.entries()) {
const route = this.timetable.getRoute(routeId)!;
let currentTrip: CurrentTrip | undefined = undefined;
const hopOnIndex = route.stopIndices.get(hopOnStop)!;
// for each stop in the route starting with the hop-on one
for (let i = hopOnIndex; i < route.stops.length; i++) {
const currentStop = route.stops[i]!;
const stopNumbers = route.stops.length;
if (currentTrip !== undefined) {
const currentArrivalIndex =
(currentTrip.trip * stopNumbers + i) * 2;
const currentArrivalTime = Time.fromMinutes(
route.stopTimes[currentArrivalIndex]!,
);
const currentDropOffType = route.pickUpDropOffTypes[i * 2 + 1];
const earliestArrivalAtCurrentStop =
earliestArrivals.get(currentStop)?.time ?? UNREACHED;
let arrivalToImprove = earliestArrivalAtCurrentStop;
if (destinations.length > 0) {
const earliestArrivalsAtDestinations: Time[] = [];
// if multiple destinations are specified, the target pruning
// should compare to the earliest arrival at any of them
for (const destinationStop of destinations) {
const earliestArrivalAtDestination =
earliestArrivals.get(destinationStop.id)?.time ?? UNREACHED;
earliestArrivalsAtDestinations.push(
earliestArrivalAtDestination,
);
}
const earliestArrivalAtDestination = Time.min(
...earliestArrivalsAtDestinations,
);
arrivalToImprove = Time.min(
earliestArrivalAtCurrentStop,
earliestArrivalAtDestination,
);
}
if (
currentDropOffType !== NOT_AVAILABLE &&
currentArrivalTime.toMinutes() < arrivalToImprove.toMinutes()
) {
const bestHopOnStopIndex = route.stopIndices.get(
currentTrip.bestHopOnStop,
)!;
const bestHopOnStopDepartureIndex =
currentTrip.trip * stopNumbers * 2 + bestHopOnStopIndex * 2 + 1;
const bestHopOnDepartureTime = Time.fromMinutes(
route.stopTimes[bestHopOnStopDepartureIndex]!,
);
arrivalsAtCurrentRound.set(currentStop, {
time: currentArrivalTime,
legNumber: round,
origin: currentTrip.origin,
leg: {
from: this.stopsIndex.findStopById(
currentTrip.bestHopOnStop,
)!,
to: this.stopsIndex.findStopById(currentStop)!,
departureTime: bestHopOnDepartureTime,
arrivalTime: currentArrivalTime,
route: this.timetable.getServiceRoute(route.serviceRouteId)!,
},
});
earliestArrivals.set(currentStop, {
time: currentArrivalTime,
legNumber: round,
origin: currentTrip.origin,
});
markedStops.add(currentStop);
}
}
// check if we can catch a previous trip at the current stop
// if there was no current trip, find the first one reachable
const earliestArrivalOnPreviousRound =
arrivalsAtPreviousRound.get(currentStop)?.time;
if (
earliestArrivalOnPreviousRound !== undefined &&
(currentTrip === undefined ||
earliestArrivalOnPreviousRound.toMinutes() <=
route.stopTimes[(currentTrip.trip * stopNumbers + i) * 2]!)
) {
const earliestTrip = this.timetable.findEarliestTrip(
route,
currentStop,
currentTrip?.trip,
earliestArrivalOnPreviousRound,
);
if (earliestTrip !== undefined) {
currentTrip = {
trip: earliestTrip,
// we need to keep track of the best hop-on stop to reconstruct the route at the end
bestHopOnStop: currentStop,
origin:
arrivalsAtPreviousRound.get(currentStop)?.origin ??
currentStop,
};
}
}
}
}
this.considerTransfers(
query,
markedStops,
arrivalsAtCurrentRound,
earliestArrivals,
round,
);
if (markedStops.size === 0) break;
}
return new Result(
query,
earliestArrivals,
earliestArrivalsPerRound,
this.stopsIndex,
);
}
}