UNPKG

minotor

Version:

A lightweight client-side transit routing library.

148 lines (136 loc) 5.47 kB
import { StopId } from '../stops/stops.js'; import { StopsIndex } from '../stops/stopsIndex.js'; import { PickUpDropOffTypes } from '../timetable/route.js'; import { Duration, Time } from '../timetable/time.js'; import { Timetable, TransferTypes } from '../timetable/timetable.js'; /** * An access path from the query origin to an initial boarding stop. * * Equivalent origin stops (reached at zero cost) have no `duration`. * Stops reachable via a timed walking transfer carry a `duration` in minutes. */ export type AccessPoint = { fromStopId: StopId; toStopId: StopId; duration: Duration; }; /** * Collects access paths from a query origin and resolves the set of * distinct departure-time slots for Range RAPTOR. */ export class AccessFinder { private readonly timetable: Timetable; private readonly stopsIndex: StopsIndex; constructor(timetable: Timetable, stopsIndex: StopsIndex) { this.timetable = timetable; this.stopsIndex = stopsIndex; } /** * Returns every initial access path from the query origin: equivalent stops * (no duration) plus every stop reachable via a single timed walking transfer * (REQUIRES_MINIMAL_TIME), keeping the shortest walk when multiple origins * can reach the same stop. * * @param origin Origin stop ID. * @param fallbackMinTransferTime Transfer time used when a walking transfer * has no explicit `minTransferTime` in the timetable data. */ collectAccessPaths( queryOrigin: StopId, fallbackMinTransferTime: Duration, ): AccessPoint[] { const equivalentOrigins = this.stopsIndex .equivalentStops(queryOrigin) .map((stop) => stop.id); const accessPaths = new Map<StopId, AccessPoint>(); for (const origin of equivalentOrigins) { const existingAccess = accessPaths.get(origin); if (existingAccess === undefined || existingAccess.duration > 0) { accessPaths.set(origin, { fromStopId: origin, toStopId: origin, duration: 0, }); } for (const transfer of this.timetable.getTransfers(origin)) { if (transfer.type === TransferTypes.REQUIRES_MINIMAL_TIME) { const duration = transfer.minTransferTime ?? fallbackMinTransferTime; const existingAccess = accessPaths.get(transfer.destination); // Keep the shortest walk to maximize the set of reachable trips. if ( existingAccess === undefined || (existingAccess.duration && duration < existingAccess.duration) ) { accessPaths.set(transfer.destination, { fromStopId: origin, toStopId: transfer.destination, duration, }); } } } } return Array.from(accessPaths.values()); } /** * Collects all distinct origin departure times within `[from, to]` * (inclusive) and, for each slot, the specific access paths that directly * induce it — i.e. paths whose boarded stop has a boardable trip departing * at exactly `depTime + path.duration`. * * Returned array is sorted **latest-first**. The Range RAPTOR outer loop * seeds only the responsible paths for each slot, avoiding redundant * exploration of access stops whose boarding opportunities belong to a * later slot and whose journeys would therefore be dominated by it. * * @param accessPaths Access paths from the origin to initial boarding stops. * @param from Earliest origin departure time (inclusive). * @param to Latest origin departure time (inclusive). */ collectDepartureTimes( accessPaths: AccessPoint[], from: Time, to: Time, ): { depTime: Time; legs: AccessPoint[] }[] { // Map from origin-departure-time → the set of access paths that induce it. const slotMap = new Map<Time, Set<AccessPoint>>(); for (const path of accessPaths) { const { toStopId } = path; // Trips from this stop must depart in [from + duration, to + duration] // so that the corresponding origin departure (dep - duration) falls in // [from, to]. const searchFrom = from + path.duration; const searchTo = to + path.duration; for (const route of this.timetable.routesPassingThrough(toStopId)) { for (const stopIndex of route.stopRouteIndices(toStopId)) { let tripIndex = route.findEarliestTrip(stopIndex, searchFrom); if (tripIndex === undefined) continue; const nbTrips = route.getNbTrips(); while (tripIndex < nbTrips) { const dep = route.departureFrom(stopIndex, tripIndex); if (dep > searchTo) break; if ( route.pickUpTypeFrom(stopIndex, tripIndex) !== PickUpDropOffTypes.NOT_AVAILABLE ) { const t = dep - path.duration; let paths = slotMap.get(t); if (paths === undefined) { slotMap.set(t, (paths = new Set<AccessPoint>())); } paths.add(path); } tripIndex++; } } } } if (slotMap.size === 0) return []; // Sort descending so the outer loop processes latest departures first. const sorted = Array.from(slotMap.entries()).sort(([a], [b]) => b - a); return sorted.map(([depTime, paths]) => ({ depTime, legs: Array.from(paths), })); } }