minotor
Version:
A lightweight client-side transit routing library.
320 lines (291 loc) • 9.37 kB
text/typescript
import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
import { StopId } from '../stops/stops.js';
import { Duration } from './duration.js';
import {
deserializeRoutesAdjacency,
deserializeServiceRoutesMap,
deserializeStopsAdjacency,
serializeRoutesAdjacency,
serializeServiceRoutesMap,
serializeStopsAdjacency,
} from './io.js';
import { Timetable as ProtoTimetable } from './proto/timetable.js';
import { Time } from './time.js';
// Identifies all trips of a given service route sharing the same list of stops.
export type RouteId = string;
export const REGULAR = 0;
export const NOT_AVAILABLE = 1;
export const MUST_PHONE_AGENCY = 2;
export const MUST_COORDINATE_WITH_DRIVER = 3;
export type PickUpDropOffType =
| 0 // REGULAR
| 1 // NOT_AVAILABLE
| 2 // MUST_PHONE_AGENCY
| 3; // MUST_COORDINATE_WITH_DRIVER
export type Route = {
/**
* Arrivals and departures encoded as minutes from midnight.
* Format: [arrival1, departure1, arrival2, departure2, etc.]
*/
stopTimes: Uint16Array;
/**
* PickUp and DropOff types represented as a binary Uint8Array.
* Values:
* 0: REGULAR
* 1: NOT_AVAILABLE
* 2: MUST_PHONE_AGENCY
* 3: MUST_COORDINATE_WITH_DRIVER
* Format: [pickupTypeStop1, dropOffTypeStop1, pickupTypeStop2, dropOffTypeStop2, etc.]
*/
pickUpDropOffTypes: Uint8Array;
/**
* A binary array of stopIds in the route.
* [stop1, stop2, stop3,...]
*/
stops: Uint32Array;
/**
* A reverse mapping of each stop with their index in the route:
* {
* 4: 0,
* 5: 1,
* ...
* }
*/
stopIndices: Map<StopId, number>;
/**
* The identifier of the route as a service shown to users.
*/
serviceRouteId: ServiceRouteId;
// TODO Add tripIds for real-time support
};
export type RoutesAdjacency = Map<RouteId, Route>;
export type TransferType =
| 'RECOMMENDED'
| 'GUARANTEED'
| 'REQUIRES_MINIMAL_TIME'
| 'IN_SEAT';
export type Transfer = {
destination: StopId;
type: TransferType;
minTransferTime?: Duration;
};
export type StopsAdjacency = Map<
StopId,
{
transfers: Transfer[];
routes: RouteId[];
}
>;
export type ServiceRouteId = string;
export type RouteType =
| 'TRAM'
| 'SUBWAY'
| 'RAIL'
| 'BUS'
| 'FERRY'
| 'CABLE_TRAM'
| 'AERIAL_LIFT'
| 'FUNICULAR'
| 'TROLLEYBUS'
| 'MONORAIL';
export type ServiceRoute = {
type: RouteType;
name: string;
};
// 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 ServiceRoutesMap = Map<ServiceRouteId, ServiceRoute>;
// a trip index corresponds to the index of the
// first stop time in the trip modulo the number of stops
// in the given route
type TripIndex = number;
export const ALL_TRANSPORT_MODES: RouteType[] = [
'TRAM',
'SUBWAY',
'RAIL',
'BUS',
'FERRY',
'CABLE_TRAM',
'AERIAL_LIFT',
'FUNICULAR',
'TROLLEYBUS',
'MONORAIL',
];
export const CURRENT_VERSION = '0.0.3';
/**
* The internal transit timetable format
* reuses some GTFS concepts for the sake of simplicity for now.
*/
export class Timetable {
private readonly stopsAdjacency: StopsAdjacency;
private readonly routesAdjacency: RoutesAdjacency;
private readonly routes: ServiceRoutesMap;
constructor(
stopsAdjacency: StopsAdjacency,
routesAdjacency: RoutesAdjacency,
routes: ServiceRoutesMap,
) {
this.stopsAdjacency = stopsAdjacency;
this.routesAdjacency = routesAdjacency;
this.routes = routes;
}
/**
* Serializes the Timetable into a binary protobuf.
*
* @returns {Uint8Array} - The serialized binary data.
*/
serialize(): Uint8Array {
const protoTimetable = {
version: CURRENT_VERSION,
stopsAdjacency: serializeStopsAdjacency(this.stopsAdjacency),
routesAdjacency: serializeRoutesAdjacency(this.routesAdjacency),
routes: serializeServiceRoutesMap(this.routes),
};
const writer = new BinaryWriter();
ProtoTimetable.encode(protoTimetable, writer);
return writer.finish();
}
/**
* Deserializes a binary protobuf into a Timetable object.
*
* @param {Uint8Array} data - The binary data to deserialize.
* @returns {Timetable} - The deserialized Timetable object.
*/
static fromData(data: Uint8Array): Timetable {
const reader = new BinaryReader(data);
const protoTimetable = ProtoTimetable.decode(reader);
if (protoTimetable.version !== CURRENT_VERSION) {
throw new Error(
`Unsupported timetable version ${protoTimetable.version}`,
);
}
return new Timetable(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deserializeStopsAdjacency(protoTimetable.stopsAdjacency!),
deserializeRoutesAdjacency(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
protoTimetable.routesAdjacency!,
),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
deserializeServiceRoutesMap(protoTimetable.routes!),
);
}
getRoutesThroughStop(stopId: StopId): RouteId[] {
const stopAdjacency = this.stopsAdjacency.get(stopId);
if (!stopAdjacency) {
return [];
}
return stopAdjacency.routes;
}
getRoute(routeId: RouteId): Route | undefined {
return this.routesAdjacency.get(routeId);
}
getTransfers(stopId: StopId): Transfer[] {
return this.stopsAdjacency.get(stopId)?.transfers ?? [];
}
/**
* Finds routes that are reachable from a set of stop IDs.
* Also identifies the first stop available to hop on each route among
* the input stops.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
findReachableRoutes(
fromStops: Set<StopId>,
transportModes: RouteType[] = ALL_TRANSPORT_MODES,
): Map<RouteId, StopId> {
const reachableRoutes = new Map<RouteId, StopId>();
for (const stop of fromStops) {
const validRoutes = this.stopsAdjacency
.get(stop)
?.routes.filter((routeId) => {
const serviceRoute = this.getServiceRouteFromRouteId(routeId);
if (!serviceRoute) {
return false;
}
return transportModes.includes(serviceRoute.type);
});
for (const routeId of validRoutes || []) {
const hopOnStop = reachableRoutes.get(routeId);
if (hopOnStop) {
// Checks if the existing hop on stop is before the current stop
const routeStopIndices =
this.routesAdjacency.get(routeId)!.stopIndices;
const stopIndex = routeStopIndices.get(stop)!;
const hopOnStopIndex = routeStopIndices.get(hopOnStop)!;
if (stopIndex < hopOnStopIndex) {
// if the current stop is before the existing hop on stop, replace it
reachableRoutes.set(routeId, stop);
}
} else {
reachableRoutes.set(routeId, stop);
}
}
}
return reachableRoutes;
}
getServiceRouteFromRouteId(routeId: RouteId): ServiceRoute | undefined {
const route = this.routesAdjacency.get(routeId);
if (!route) {
console.warn(`Route ${routeId} not found.`);
return undefined;
}
return this.routes.get(route.serviceRouteId);
}
getServiceRoute(serviceRouteId: ServiceRouteId): ServiceRoute | undefined {
return this.routes.get(serviceRouteId);
}
/**
* 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.
*/
findEarliestTrip(
route: Route,
stopId: StopId,
beforeTrip?: TripIndex,
after: Time = Time.origin(),
): TripIndex | undefined {
const stopIndex = route.stopIndices.get(stopId)!;
const stopsNumber = route.stops.length;
if (beforeTrip === undefined) {
for (
let tripIndex = 0;
tripIndex < route.stopTimes.length / stopsNumber;
tripIndex++
) {
const stopTimeIndex = tripIndex * stopsNumber + stopIndex;
const departure = route.stopTimes[stopTimeIndex * 2 + 1]!;
const pickUpType = route.pickUpDropOffTypes[stopTimeIndex * 2]!;
if (departure >= after.toMinutes() && pickUpType !== NOT_AVAILABLE) {
return tripIndex;
}
}
return undefined;
} else {
let earliestTripIndex: TripIndex | undefined;
let earliestDeparture: Time | undefined;
for (
let tripIndex = beforeTrip; // ?? route.stopTimes.length / stopsNumber - 1;
tripIndex >= 0;
tripIndex--
) {
const stopTimeIndex = tripIndex * stopsNumber + stopIndex;
const departure = route.stopTimes[stopTimeIndex * 2 + 1]!;
const pickUpType = route.pickUpDropOffTypes[stopTimeIndex * 2]!;
if (departure < after.toMinutes()) {
break;
}
if (
pickUpType !== NOT_AVAILABLE &&
(earliestDeparture === undefined ||
departure < earliestDeparture.toMinutes())
) {
earliestTripIndex = tripIndex;
earliestDeparture = Time.fromMinutes(departure);
}
}
return earliestTripIndex;
}
}
}