UNPKG

minotor

Version:

A lightweight client-side transit routing library.

333 lines (305 loc) 10.6 kB
import { StopId } from '../stops/stops.js'; import { SerializedRoute } from './io.js'; import { Time } from './time.js'; import { ServiceRouteId } from './timetable.js'; /** * An internal identifier for routes. * Not to mix with the ServiceRouteId which corresponds to the GTFS RouteId. * This one is used for identifying groups of trips * from a service route sharing the same list of stops. */ export type RouteId = number; /** * Details about the pickup and drop-off modalities at each stop in each trip of a route. */ export type PickUpDropOffType = | 'REGULAR' | 'NOT_AVAILABLE' | 'MUST_PHONE_AGENCY' | 'MUST_COORDINATE_WITH_DRIVER'; export const REGULAR = 0; export const NOT_AVAILABLE = 1; export const MUST_PHONE_AGENCY = 2; export const MUST_COORDINATE_WITH_DRIVER = 3; /* * A trip index corresponds to the index of the * first stop time in the trip divided by the number of stops * in the given route */ export type TripIndex = number; const pickUpDropOffTypeMap: PickUpDropOffType[] = [ 'REGULAR', 'NOT_AVAILABLE', 'MUST_PHONE_AGENCY', 'MUST_COORDINATE_WITH_DRIVER', ]; /** * Converts a numerical representation of a pick-up/drop-off type * into its corresponding string representation. * * @param numericalType - The numerical value representing the pick-up/drop-off type. * @returns The corresponding PickUpDropOffType as a string. * @throws An error if the numerical type is invalid. */ const toPickupDropOffType = (numericalType: number): PickUpDropOffType => { const type = pickUpDropOffTypeMap[numericalType]; if (!type) { throw new Error(`Invalid pickup/drop-off type ${numericalType}`); } return type; }; /** * A route identifies all trips of a given service route sharing the same list of stops. */ export class Route { /** * Arrivals and departures encoded as minutes from midnight. * Format: [arrival1, departure1, arrival2, departure2, etc.] */ private readonly stopTimes: Uint16Array; /** * PickUp and DropOff types represented as a 2-bit encoded Uint8Array. * Values (2 bits each): * 0: REGULAR * 1: NOT_AVAILABLE * 2: MUST_PHONE_AGENCY * 3: MUST_COORDINATE_WITH_DRIVER * * Encoding format: Each byte contains 2 pickup/drop-off pairs (4 bits each) * Bit layout per byte: [pickup_1 (2 bits)][drop_off_1 (2 bits)][pickup_0 (2 bits)][drop_off_0 (2 bits)] * Example: For stops 0 and 1 in a trip, one byte encodes all 4 values */ private readonly pickUpDropOffTypes: Uint8Array; /** * A binary array of stopIds in the route. * [stop1, stop2, stop3,...] */ public readonly stops: Uint32Array; /** * A reverse mapping of each stop with their index in the route: * { * 4: 0, * 5: 1, * ... * } */ private readonly stopIndices: Map<StopId, number>; /** * The identifier of the route as a service shown to users. */ private readonly serviceRouteId: ServiceRouteId; /** * The total number of stops in the route. */ private readonly nbStops: number; /** * The total number of trips in the route. */ private readonly nbTrips: number; constructor( stopTimes: Uint16Array, pickUpDropOffTypes: Uint8Array, stops: Uint32Array, serviceRouteId: ServiceRouteId, ) { this.stopTimes = stopTimes; this.pickUpDropOffTypes = pickUpDropOffTypes; this.stops = stops; this.serviceRouteId = serviceRouteId; this.nbStops = stops.length; this.nbTrips = this.stopTimes.length / (this.stops.length * 2); this.stopIndices = new Map<number, number>(); for (let i = 0; i < stops.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.stopIndices.set(stops[i]!, i); } } /** * Serializes the Route into binary arrays. * * @returns The serialized binary data. */ serialize(): SerializedRoute { return { stopTimes: this.stopTimes, pickUpDropOffTypes: this.pickUpDropOffTypes, stops: this.stops, serviceRouteId: this.serviceRouteId, }; } /** * Checks if stop A is before stop B in the route. * * @param stopA - The StopId of the first stop. * @param stopB - The StopId of the second stop. * @returns True if stop A is before stop B, false otherwise. */ isBefore(stopA: StopId, stopB: StopId): boolean { const stopAIndex = this.stopIndices.get(stopA); if (stopAIndex === undefined) { throw new Error( `Stop index ${stopAIndex} not found in route ${this.serviceRouteId}`, ); } const stopBIndex = this.stopIndices.get(stopB); if (stopBIndex === undefined) { throw new Error( `Stop index ${stopBIndex} not found in route ${this.serviceRouteId}`, ); } return stopAIndex < stopBIndex; } /** * Retrieves the number of stops in the route. * * @returns The total number of stops in the route. */ getNbStops(): number { return this.nbStops; } /** * Finds the ServiceRouteId of the route. It corresponds the identifier * of the service shown to the end user as a route. * * @returns The ServiceRouteId of the route. */ serviceRoute(): ServiceRouteId { return this.serviceRouteId; } /** * Retrieves the arrival time at a specific stop for a given trip. * * @param stopId - The identifier of the stop. * @param tripIndex - The index of the trip. * @returns The arrival time at the specified stop and trip as a Time object. */ arrivalAt(stopId: StopId, tripIndex: TripIndex): Time { const arrivalIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2; const arrival = this.stopTimes[arrivalIndex]; if (arrival === undefined) { throw new Error( `Arrival time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`, ); } return Time.fromMinutes(arrival); } /** * Retrieves the departure time at a specific stop for a given trip. * * @param stopId - The identifier of the stop. * @param tripIndex - The index of the trip. * @returns The departure time at the specified stop and trip as a Time object. */ departureFrom(stopId: StopId, tripIndex: TripIndex): Time { const departureIndex = (tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1; const departure = this.stopTimes[departureIndex]; if (departure === undefined) { throw new Error( `Departure time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`, ); } return Time.fromMinutes(departure); } /** * Retrieves the pick-up type for a specific stop and trip. * * @param stopId - The identifier of the stop. * @param tripIndex - The index of the trip. * @returns The pick-up type at the specified stop and trip. */ pickUpTypeFrom(stopId: StopId, tripIndex: TripIndex): PickUpDropOffType { const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId); const byteIndex = Math.floor(globalIndex / 2); const isSecondPair = globalIndex % 2 === 1; const byte = this.pickUpDropOffTypes[byteIndex]; if (byte === undefined) { throw new Error( `Pick up type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`, ); } const pickUpValue = isSecondPair ? (byte >> 6) & 0x03 // Upper 2 bits for second pair : (byte >> 2) & 0x03; // Bits 2-3 for first pair return toPickupDropOffType(pickUpValue); } /** * Retrieves the drop-off type for a specific stop and trip. * * @param stopId - The identifier of the stop. * @param tripIndex - The index of the trip. * @returns The drop-off type at the specified stop and trip. */ dropOffTypeAt(stopId: StopId, tripIndex: TripIndex): PickUpDropOffType { const globalIndex = tripIndex * this.stops.length + this.stopIndex(stopId); const byteIndex = Math.floor(globalIndex / 2); const isSecondPair = globalIndex % 2 === 1; const byte = this.pickUpDropOffTypes[byteIndex]; if (byte === undefined) { throw new Error( `Drop off type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`, ); } const dropOffValue = isSecondPair ? (byte >> 4) & 0x03 // Bits 4-5 for second pair : byte & 0x03; // Lower 2 bits for first pair return toPickupDropOffType(dropOffValue); } /** * Finds the earliest trip that can be taken from a specific stop on a given route, * optionally constrained by a latest trip index and a time before which the trip * should not depart. * * * @param stopId - The StopId of the stop where the trip should be found. * @param [after=Time.origin()] - The earliest time after which the trip should depart. * If not provided, searches all available trips. * @param [beforeTrip] - (Optional) The index of the trip before which the search should be constrained. * If not provided, searches all available trips. * @returns The index of the earliest trip meeting the criteria, or undefined if no such trip is found. */ findEarliestTrip( stopId: StopId, after: Time = Time.origin(), beforeTrip?: TripIndex, ): TripIndex | undefined { if (this.nbTrips <= 0) return undefined; let hi = this.nbTrips - 1; if (beforeTrip !== undefined) hi = Math.min(hi, beforeTrip - 1); if (hi < 0) return undefined; let lo = 0; let lb = -1; while (lo <= hi) { const mid = (lo + hi) >>> 1; const depMid = this.departureFrom(stopId, mid); if (depMid.isBefore(after)) { lo = mid + 1; } else { lb = mid; hi = mid - 1; } } if (lb === -1) return undefined; for (let t = lb; t < (beforeTrip ?? this.nbTrips); t++) { const pickup = this.pickUpTypeFrom(stopId, t); if (pickup !== 'NOT_AVAILABLE') { return t; } } return undefined; } /** * Retrieves the index of a stop within the route. * @param stopId The StopId of the stop to locate in the route. * @returns The index of the stop in the route. */ public stopIndex(stopId: StopId): number { const stopIndex = this.stopIndices.get(stopId); if (stopIndex === undefined) { throw new Error( `Stop index for ${stopId} not found in route ${this.serviceRouteId}`, ); } return stopIndex; } }