UNPKG

minotor

Version:

A lightweight client-side transit routing library.

313 lines (290 loc) 11.4 kB
import { StopId } from '../stops/stops.js'; import { Duration, Time } from '../timetable/time.js'; import { Result } from './result.js'; import { Route } from './route.js'; import { Arrival } from './state.js'; /** * A single departure-time iteration that produced at least one Pareto-optimal * journey to this result's destination set. */ export type ParetoRun = { /** Departure time from the origin (minutes from midnight) for this run. */ readonly departureTime: Time; /** Full RAPTOR result for this departure time — use it to reconstruct routes. */ readonly result: Result; }; /** * An {@link Arrival} enriched with the travel duration from the origin. * * Returned by duration-based methods on {@link RangeResult} so callers * receive both the absolute arrival time with transfer count *and* the total * travel time that was optimized over. */ export type ArrivalWithDuration = Arrival & { /** Total travel time from origin departure to stop arrival (minutes). */ readonly duration: Duration; }; /** * The result of a Range RAPTOR query. * * Contains the complete Pareto-optimal set of journeys for a resolved * destination set, **or** the full per-departure-time routing state when no * destinations were provided (full-network / isochrone mode). * * **Pareto dominance**: journey J1 dominates J2 iff * `τdep(J1) ≥ τdep(J2) AND τarr(J1) ≤ τarr(J2)` * (with at least one strict inequality). * * Runs are ordered **latest-departure-first**: each successive run departs * strictly earlier *and* arrives strictly earlier than the previous one, * forming the classic staircase Pareto frontier. * * **Full-network mode** (empty `destinations`): when no destinations are * supplied to the range query every departure slot in the window becomes its * own run, because destination-based Pareto pruning cannot be applied. * In this mode the destination-specific helpers ({@link getRoutes}, * {@link bestRoute}, {@link latestDepartureRoute}, {@link fastestRoute}) * return empty results; use {@link allEarliestArrivals}, * {@link allShortestDurations}, {@link earliestArrivalAt}, or * {@link shortestDurationTo} instead. * * Destination handling is delegated to {@link Result}, which expands * equivalent stops when reconstructing routes or looking up arrivals. */ export class RangeResult { private readonly _runs: readonly ParetoRun[]; private readonly _destinations: ReadonlySet<StopId>; constructor(runs: ParetoRun[], destinations: ReadonlySet<StopId>) { this._runs = runs; this._destinations = destinations; } /** The resolved destination stop IDs for this result. */ get destinations(): ReadonlySet<StopId> { return this._destinations; } private normalizeTargets(to?: StopId | Set<StopId>): Set<StopId> { if (to instanceof Set) return new Set(to); if (to !== undefined) return new Set([to]); return new Set(this._destinations); } /** * Returns all non-dominated routes to this result's default destination set, * ordered from the earliest departure to the latest departure. * * Each route in the list departs strictly earlier *and* arrives strictly * earlier than its predecessor. * * Returns an empty array when no destinations were provided (full-network * mode). Use {@link allEarliestArrivals} or {@link allShortestDurations} * to query individual stops in that case. */ getRoutes(): Route[] { const routes: Route[] = []; for (const { result } of this._runs) { const route = result.bestRoute(); if (route !== undefined) routes.push(route); } return routes.reverse(); } /** * The route that arrives **earliest** at the given stop(s) across all * Pareto-optimal runs. * * When two runs achieve the same arrival time at the target, the one with * the **later departure** is preferred — you wait at the origin rather than * at a transit stop. * * Defaults to this result's own destination stop(s) when `to` is omitted. * Always pass an explicit `to` stop when operating in full-network mode * (no destinations), otherwise `undefined` is returned. * * @param to Optional destination stop ID or set of stop IDs. * @returns The reconstructed {@link Route} with the earliest arrival, * or `undefined` if the target is unreachable in every run. */ bestRoute(to?: StopId | Set<StopId>): Route | undefined { const targetStops = this.normalizeTargets(to); let bestRun: ParetoRun | undefined; let bestArrival: Time | undefined; for (const run of this._runs) { for (const stopId of targetStops) { const arrival = run.result.arrivalAt(stopId); if (arrival === undefined) continue; if (bestArrival === undefined || arrival.arrival < bestArrival) { bestArrival = arrival.arrival; bestRun = run; } } } return bestRun?.result.bestRoute(targetStops); } /** * The route with the **latest possible departure** from the origin among all * Pareto-optimal journeys in the window. * * This is the journey that lets you leave the origin as late as possible. * It does **not** necessarily achieve the earliest arrival — for that, use * {@link bestRoute}. For the shortest travel duration, use * {@link fastestRoute}. * * Defaults to this result's own destination stop(s) when `to` is omitted. * Always pass an explicit `to` stop when operating in full-network mode * (no destinations), otherwise `undefined` is returned. * * @param to Optional destination stop ID or set of stop IDs. * @returns The reconstructed {@link Route} with the latest departure, * or `undefined` if the target is unreachable in every run. */ latestDepartureRoute(to?: StopId | Set<StopId>): Route | undefined { const targetStops = this.normalizeTargets(to); for (const { result } of this._runs) { const route = result.bestRoute(targetStops); if (route !== undefined) return route; } return undefined; } /** * Reconstructs the **fastest** route to the given stop(s) — the journey with * the shortest travel duration (arrival time − origin departure time) across * all Pareto-optimal runs. * * Unlike {@link bestRoute}, which returns the route that departs as late as * possible while still arriving early, this method minimizes total time * spent traveling. * * Defaults to this result's own destination stop(s) when `to` is omitted. * Always pass an explicit `to` stop when operating in full-network mode * (no destinations), otherwise `undefined` is returned. * * @param to Optional destination stop ID or set of stop IDs. * @returns The reconstructed fastest {@link Route}, or `undefined` if the * target is unreachable in every run. */ fastestRoute(to?: StopId | Set<StopId>): Route | undefined { const targetStops = this.normalizeTargets(to); let fastestRun: ParetoRun | undefined; let shortestDuration = Infinity; for (const run of this._runs) { for (const stopId of targetStops) { const arrival = run.result.arrivalAt(stopId); if (arrival === undefined) continue; const duration = arrival.arrival - run.departureTime; if (duration < shortestDuration) { shortestDuration = duration; fastestRun = run; } } } return fastestRun?.result.bestRoute(targetStops); } /** Number of Pareto-optimal journeys found. */ get size(): number { return this._runs.length; } /** * Earliest achievable arrival at a stop across all Pareto-optimal runs. * * Useful for isochrone / accessibility analysis: given this result's * departure-time frontier, how early can you reach stop `s` regardless of * which specific trip you take? * * Equivalent stops are handled by {@link Result.arrivalAt}. * * @param stop The target stop ID. * @param maxTransfers Optional upper bound on the number of transfers. */ earliestArrivalAt(stop: StopId, maxTransfers?: number): Arrival | undefined { let best: Arrival | undefined; for (const { result } of this._runs) { const arrival = result.arrivalAt(stop, maxTransfers); if ( arrival !== undefined && (best === undefined || arrival.arrival < best.arrival) ) { best = arrival; } } return best; } /** * Shortest travel duration to reach a stop across all Pareto-optimal runs. * * For each run, duration is measured from the run's origin departure time to * the earliest arrival at `stop` within that run. The minimum across all * runs is returned. * * Equivalent stops are handled by {@link Result.arrivalAt}. * * Duration is **not** monotone along the Pareto frontier — a run that * departs later may still travel faster — so every run is checked. In * practice the Pareto frontier is small, so this is O(runs). * * Returns `undefined` if `stop` is unreachable in every run. * * @param stop The target stop ID. * @param maxTransfers Optional upper bound on the number of transfers. */ shortestDurationTo( stop: StopId, maxTransfers?: number, ): ArrivalWithDuration | undefined { let shortest: ArrivalWithDuration | undefined; for (const { departureTime, result } of this._runs) { const arrival = result.arrivalAt(stop, maxTransfers); if (arrival === undefined) continue; const duration = arrival.arrival - departureTime; if (shortest === undefined || duration < shortest.duration) { shortest = { ...arrival, duration }; } } return shortest; } /** * Shortest travel duration to **every reachable stop** across all * Pareto-optimal runs, as a single `Map<StopId, DurationArrival>`. */ allShortestDurations(): Map<StopId, ArrivalWithDuration> { const durations = new Map<StopId, ArrivalWithDuration>(); for (const { departureTime, result } of this._runs) { for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) { const duration = arrival - departureTime; const existing = durations.get(stop); if (existing === undefined || duration < existing.duration) { durations.set(stop, { arrival, legNumber, duration }); } } } return durations; } /** * Earliest achievable arrival at **every reachable stop** across all * Pareto-optimal runs, as a single `Map<StopId, Arrival>`. */ allEarliestArrivals(): Map<StopId, Arrival> { const arrivals = new Map<StopId, Arrival>(); for (const { result } of this._runs) { for (const { stop, arrival, legNumber, } of result.routingState.arrivals()) { const existing = arrivals.get(stop); if (existing === undefined || arrival < existing.arrival) { arrivals.set(stop, { arrival, legNumber }); } } } return arrivals; } /** * Iterates over all Pareto-optimal `(departureTime, result)` pairs, * ordered from the latest departure to the earliest departure. */ [Symbol.iterator](): IterableIterator<ParetoRun> { return this._runs[Symbol.iterator]() as IterableIterator<ParetoRun>; } }