UNPKG

minotor

Version:

A lightweight client-side transit routing library.

465 lines (426 loc) 14.7 kB
/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire'; import { StopId } from '../stops/stops.js'; import { deserializeRoutesAdjacency, deserializeServiceRoutesMap, deserializeStopsAdjacency, deserializeTripTransfers, serializeRoutesAdjacency, serializeServiceRoutesMap, serializeStopsAdjacency, serializeTripTransfers, } from './io.js'; import { Timetable as ProtoTimetable } from './proto/v1/timetable.js'; import { PickUpDropOffTypes, Route, RouteId, StopRouteIndex, TripRouteIndex, } from './route.js'; import { Duration, DURATION_ZERO, Time, TIME_ORIGIN } from './time.js'; import { encode, TripStopId } from './tripStopId.js'; export const TransferTypes = { RECOMMENDED: 1, GUARANTEED: 2, REQUIRES_MINIMAL_TIME: 3, IN_SEAT: 4, } as const; export type TransferType = (typeof TransferTypes)[keyof typeof TransferTypes]; export type TransferTypeString = keyof typeof TransferTypes; export type Transfer = { destination: StopId; type: TransferType; minTransferTime?: Duration; }; export type TripStop = { stopIndex: StopRouteIndex; routeId: RouteId; tripIndex: TripRouteIndex; }; export type StopAdjacency = { transfers?: Transfer[]; routes: RouteId[]; }; export type TripTransfers = Map<TripStopId, TripStop[]>; export type ServiceRouteId = number; export const RouteTypes = { TRAM: 1, SUBWAY: 2, RAIL: 3, BUS: 4, FERRY: 5, CABLE_TRAM: 6, AERIAL_LIFT: 7, FUNICULAR: 8, TROLLEYBUS: 9, MONORAIL: 10, } as const; export type RouteType = (typeof RouteTypes)[keyof typeof RouteTypes]; export type RouteTypeString = keyof typeof RouteTypes; // A service refers to a collection of trips that are displayed to riders as a single service. // As opposed to a route which consists of the subset of trips from a service which shares the same list of stops. // Service is here a synonym for route in the GTFS sense. export type ServiceRoute = { type: RouteType; name: string; routes: RouteId[]; }; export type ServiceRouteInfo = Omit<ServiceRoute, 'routes'>; export const ALL_TRANSPORT_MODES: Set<RouteType> = new Set( Object.values(RouteTypes), ); const EMPTY_TRIP_BOARDINGS: TripStop[] = []; /** * The internal transit timetable format. */ export class Timetable { private readonly stopsAdjacency: StopAdjacency[]; private readonly routesAdjacency: Route[]; private readonly serviceRoutes: ServiceRoute[]; private readonly tripContinuations?: TripTransfers; private readonly guaranteedTripTransfers?: TripTransfers; private readonly activeStops: Set<StopId>; constructor( stopsAdjacency: StopAdjacency[], routesAdjacency: Route[], routes: ServiceRoute[], tripContinuations?: TripTransfers, guaranteedTripTransfers?: TripTransfers, ) { this.stopsAdjacency = stopsAdjacency; this.routesAdjacency = routesAdjacency; this.serviceRoutes = routes; this.tripContinuations = tripContinuations; this.guaranteedTripTransfers = guaranteedTripTransfers; this.activeStops = new Set<StopId>(); for (let i = 0; i < stopsAdjacency.length; i++) { const stop = stopsAdjacency[i]!; if ( stop.routes.length > 0 || (stop.transfers && stop.transfers.length > 0) ) { this.activeStops.add(i); } } } /** * Serializes the Timetable into a binary array. * * @returns The serialized binary data. */ serialize(): Uint8Array { const protoTimetable = { stopsAdjacency: serializeStopsAdjacency(this.stopsAdjacency), routesAdjacency: serializeRoutesAdjacency(this.routesAdjacency), serviceRoutes: serializeServiceRoutesMap(this.serviceRoutes), tripContinuations: serializeTripTransfers( this.tripContinuations || new Map<TripStopId, TripStop[]>(), ), guaranteedTripTransfers: serializeTripTransfers( this.guaranteedTripTransfers || new Map<TripStopId, TripStop[]>(), ), }; const writer = new BinaryWriter(); ProtoTimetable.encode(protoTimetable, writer); return writer.finish(); } /** * Deserializes a binary protobuf into a Timetable object. * * @param data - The binary data to deserialize. * @returns The deserialized Timetable object. */ static fromData(data: Uint8Array): Timetable { const reader = new BinaryReader(data); const protoTimetable = ProtoTimetable.decode(reader); return new Timetable( deserializeStopsAdjacency(protoTimetable.stopsAdjacency), deserializeRoutesAdjacency(protoTimetable.routesAdjacency), deserializeServiceRoutesMap(protoTimetable.serviceRoutes), deserializeTripTransfers(protoTimetable.tripContinuations), deserializeTripTransfers(protoTimetable.guaranteedTripTransfers), ); } /** * Checks if the given stop is active on the timetable. * An active stop is a stop reached by a route that is active on the timetable * or by a transfer reachable from an active route. * * @param stopId - The ID of the stop to check. * @returns True if the stop is active, false otherwise. */ isActive(stopId: StopId): boolean { return this.activeStops.has(stopId); } /** * Returns the total number of stops in the timetable. */ nbStops(): number { return this.stopsAdjacency.length; } /** * Retrieves the route associated with the given route ID. * * @param routeId - The ID of the route to be retrieved. * @returns The route corresponding to the provided ID, * or undefined if no such route exists. */ getRoute(routeId: RouteId): Route | undefined { return this.routesAdjacency[routeId]; } /** * Retrieves all transfer options available at the specified stop. * * @param stopId - The ID of the stop to get transfers for. * @returns An array of transfer options available at the stop. */ getTransfers(stopId: StopId): Transfer[] { const stopAdjacency = this.stopsAdjacency[stopId]; if (!stopAdjacency) { throw new Error(`Stop ID ${stopId} not found`); } return stopAdjacency.transfers || []; } /** * Retrieves all trip continuation options available at the specified stop for a given trip. * * @param stopIndex - The index in the route of the stop to get trip continuations for. * @param routeId - The ID of the route to get continuations for. * @param tripIndex - The index of the trip to get continuations for. * @returns An array of trip continuation options available at the stop for the specified trip. */ getContinuousTrips( stopIndex: StopRouteIndex, routeId: RouteId, tripIndex: TripRouteIndex, ): TripStop[] { const tripContinuations = this.tripContinuations?.get( encode(stopIndex, routeId, tripIndex), ); if (!tripContinuations) { return EMPTY_TRIP_BOARDINGS; } return tripContinuations; } /** * Retrieves the service route associated with the given route. * A service route refers to a collection of trips that are displayed * to riders as a single service. * * @param route - The route for which the service route is to be retrieved. * @returns The service route corresponding to the provided route. */ getServiceRouteInfo(route: Route): ServiceRouteInfo { const serviceRoute = this.serviceRoutes[route.serviceRoute()]; if (!serviceRoute) { throw new Error( `Service route not found for route ID: ${route.serviceRoute()}`, ); } return { type: serviceRoute.type, name: serviceRoute.name }; } /** * Finds all routes passing through a stop. * * @param stopId - The ID of the stop to find routes for. * @returns An array of routes passing through the specified stop. */ routesPassingThrough(stopId: StopId): Route[] { const stopData = this.stopsAdjacency[stopId]; if (!stopData) { return []; } const routes: Route[] = []; for (let i = 0; i < stopData.routes.length; i++) { const routeId = stopData.routes[i]!; const route = this.routesAdjacency[routeId]; if (route) { routes.push(route); } } return routes; } /** * Finds routes that are reachable from a set of stop IDs. * Also identifies the first stop index available to hop on each route among * the input stops. * * @param fromStops - The set of stop IDs to find reachable routes from. * @param transportModes - The set of transport modes to consider for reachable routes. * @returns A map of reachable routes to the first stop index available to hop on each route. */ findReachableRoutes( fromStops: Set<StopId>, transportModes: Set<RouteType> = ALL_TRANSPORT_MODES, ): Map<Route, StopRouteIndex> { const reachableRoutes = new Map<Route, StopRouteIndex>(); // Skip the per-route mode check entirely when all modes are allowed, // which is the common case. const filterByMode = transportModes !== ALL_TRANSPORT_MODES; for (const originStop of fromStops) { const stopData = this.stopsAdjacency[originStop]; if (!stopData) continue; for (let i = 0; i < stopData.routes.length; i++) { const route = this.routesAdjacency[stopData.routes[i]!]; if (!route) continue; if (filterByMode) { const serviceRoute = this.serviceRoutes[route.serviceRoute()]; if (!serviceRoute || !transportModes.has(serviceRoute.type)) continue; } const originStopIndex = route.stopRouteIndices(originStop)[0]!; const existingHopOnStopIndex = reachableRoutes.get(route); if (existingHopOnStopIndex !== undefined) { if (originStopIndex < existingHopOnStopIndex) { reachableRoutes.set(route, originStopIndex); } } else { reachableRoutes.set(route, originStopIndex); } } } return reachableRoutes; } /** * Checks if a trip transfer is guaranteed for a given trip boarding. * * @param fromTripStop - The trip stop for the trip transfer origin. * @param toTripStop - The trip stop to check if it's guaranteed. * @returns True if the trip transfer is guaranteed, false otherwise. */ isTripTransferGuaranteed( fromTripStop: TripStop, toTripStop: TripStop, ): boolean { const tripBoardingId = encode( fromTripStop.stopIndex, fromTripStop.routeId, fromTripStop.tripIndex, ); const guaranteedTransfers = this.guaranteedTripTransfers?.get(tripBoardingId); if (!guaranteedTransfers) { return false; } for (let i = 0; i < guaranteedTransfers.length; i++) { const transfer = guaranteedTransfers[i]!; if ( transfer.stopIndex === toTripStop.stopIndex && transfer.routeId === toTripStop.routeId && transfer.tripIndex === toTripStop.tripIndex ) { return true; } } return false; } /** * Finds the first trip on `route` at `stopIndex` that can be boarded, starting * from `earliestTrip` and respecting pickup availability, transfer guarantees, * and minimum transfer times. * * @param stopIndex Stop at which boarding is attempted. * @param route The route to search. * @param earliestTrip First trip index to consider. * @param after Earliest time after which boarding is allowed. * @param beforeTrip Exclusive upper bound on the trip index (omit to search all). * @param fromTripStop The alighted trip stop when transferring from another trip; * `undefined` when boarding from a walk or origin. * @param transferTime Minimum transfer time required between trips. * @returns The index of the first boardable trip, or `undefined` if none found. */ findFirstBoardableTrip( stopIndex: StopRouteIndex, route: Route, earliestTrip: TripRouteIndex, after: Time = TIME_ORIGIN, beforeTrip?: TripRouteIndex, fromTripStop?: TripStop, transferTime: Duration = DURATION_ZERO, ): TripRouteIndex | undefined { const nbTrips = route.getNbTrips(); for (let t = earliestTrip; t < (beforeTrip ?? nbTrips); t++) { const pickup = route.pickUpTypeFrom(stopIndex, t); if (pickup === PickUpDropOffTypes.NOT_AVAILABLE) { continue; } if (fromTripStop === undefined) { return t; } const isGuaranteed = this.isTripTransferGuaranteed(fromTripStop, { stopIndex, routeId: route.id, tripIndex: t, }); if (isGuaranteed) { return t; } const departure = route.departureFrom(stopIndex, t); const requiredTime = after + transferTime; if (departure >= requiredTime) { return t; } } return undefined; } /** * Retrieves all guaranteed trip transfer options available at the specified stop for a given trip. * * @param stopIndex - The index in the route of the stop to get guaranteed trip transfers for. * @param routeId - The ID of the route to get guaranteed transfers for. * @param tripIndex - The index of the trip to get guaranteed transfers for. * @returns An array of trip boarding options that are guaranteed for the specified trip. */ getGuaranteedTripTransfers( stopIndex: StopRouteIndex, routeId: RouteId, tripIndex: TripRouteIndex, ): TripStop[] { const guaranteedTripTransfers = this.guaranteedTripTransfers?.get( encode(stopIndex, routeId, tripIndex), ); if (!guaranteedTripTransfers) { return EMPTY_TRIP_BOARDINGS; } return guaranteedTripTransfers; } } export const routeTypeToString = (type: RouteType): RouteTypeString => { switch (type) { case RouteTypes.TRAM: return 'TRAM'; case RouteTypes.SUBWAY: return 'SUBWAY'; case RouteTypes.RAIL: return 'RAIL'; case RouteTypes.BUS: return 'BUS'; case RouteTypes.FERRY: return 'FERRY'; case RouteTypes.CABLE_TRAM: return 'CABLE_TRAM'; case RouteTypes.AERIAL_LIFT: return 'AERIAL_LIFT'; case RouteTypes.FUNICULAR: return 'FUNICULAR'; case RouteTypes.TROLLEYBUS: return 'TROLLEYBUS'; case RouteTypes.MONORAIL: return 'MONORAIL'; } }; export const transferTypeToString = ( type: TransferType, ): TransferTypeString => { switch (type) { case TransferTypes.RECOMMENDED: return 'RECOMMENDED'; case TransferTypes.GUARANTEED: return 'GUARANTEED'; case TransferTypes.REQUIRES_MINIMAL_TIME: return 'REQUIRES_MINIMAL_TIME'; case TransferTypes.IN_SEAT: return 'IN_SEAT'; } };