UNPKG

minotor

Version:

A lightweight client-side transit routing library.

393 lines (342 loc) 11.8 kB
import { Route as ProtoRoute, RouteType as ProtoRouteType, ServiceRoute as ProtoServiceRoute, StopAdjacency as ProtoStopAdjacency, TransferType as ProtoTransferType, TripTransferEntry as ProtoTripTransferEntry, } from './proto/v1/timetable.js'; import { Route } from './route.js'; import { RouteType, RouteTypes, ServiceRoute, ServiceRouteId, StopAdjacency, Transfer, TransferType, TransferTypes, TripStop, TripTransfers, } from './timetable.js'; import { decode, encode, TripStopId } from './tripStopId.js'; export type SerializedRoute = { stopTimes: Uint16Array; pickupDropOffTypes: Uint8Array; stops: Uint32Array; serviceRouteId: ServiceRouteId; }; const isLittleEndian = (() => { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint32(0, 0x12345678); return new Uint8Array(buffer)[0] === 0x78; })(); const STANDARD_ENDIANNESS = true; // true = little-endian const uint32ArrayToBytes = (array: Uint32Array): Uint8Array => { if (isLittleEndian === STANDARD_ENDIANNESS) { return new Uint8Array(array.buffer, array.byteOffset, array.byteLength); } // If endianness doesn't match, we need to swap byte order const result = new Uint8Array(array.length * 4); const view = new DataView(result.buffer); for (let i = 0; i < array.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion view.setUint32(i * 4, array[i]!, STANDARD_ENDIANNESS); } return result; }; const bytesToUint32Array = (bytes: Uint8Array): Uint32Array => { if (bytes.byteLength % 4 !== 0) { throw new Error( 'Byte array length must be a multiple of 4 to convert to Uint32Array', ); } // If system endianness matches our standard, we can create a view directly if (isLittleEndian === STANDARD_ENDIANNESS) { return new Uint32Array( bytes.buffer, bytes.byteOffset, bytes.byteLength / 4, ); } // If endianness doesn't match, we need to swap byte order const result = new Uint32Array(bytes.byteLength / 4); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); for (let i = 0; i < result.length; i++) { result[i] = view.getUint32(i * 4, STANDARD_ENDIANNESS); } return result; }; const uint16ArrayToBytes = (array: Uint16Array): Uint8Array => { if (isLittleEndian === STANDARD_ENDIANNESS) { return new Uint8Array(array.buffer, array.byteOffset, array.byteLength); } // If endianness doesn't match, we need to swap byte order const result = new Uint8Array(array.length * 2); const view = new DataView(result.buffer); for (let i = 0; i < array.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion view.setUint16(i * 2, array[i]!, STANDARD_ENDIANNESS); } return result; }; const bytesToUint16Array = (bytes: Uint8Array): Uint16Array => { if (bytes.byteLength % 2 !== 0) { throw new Error( 'Byte array length must be a multiple of 2 to convert to Uint16Array', ); } // If system endianness matches our standard, we can create a view directly if (isLittleEndian === STANDARD_ENDIANNESS) { return new Uint16Array( bytes.buffer, bytes.byteOffset, bytes.byteLength / 2, ); } // If endianness doesn't match, we need to swap byte order const result = new Uint16Array(bytes.byteLength / 2); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); for (let i = 0; i < result.length; i++) { result[i] = view.getUint16(i * 2, STANDARD_ENDIANNESS); } return result; }; export const serializeStopsAdjacency = ( stopsAdjacency: StopAdjacency[], ): ProtoStopAdjacency[] => { return stopsAdjacency.map((value) => { return { transfers: value.transfers ? value.transfers.map((transfer) => ({ destination: transfer.destination, type: serializeTransferType(transfer.type), ...(transfer.minTransferTime !== undefined && { minTransferTime: transfer.minTransferTime, }), })) : [], routes: value.routes, }; }); }; export const serializeRoutesAdjacency = ( routesAdjacency: Route[], ): ProtoRoute[] => { const protoRoutesAdjacency: ProtoRoute[] = []; routesAdjacency.forEach((route: Route) => { const routeData = route.serialize(); protoRoutesAdjacency.push({ stopTimes: uint16ArrayToBytes(routeData.stopTimes), pickupDropOffTypes: routeData.pickupDropOffTypes, stops: uint32ArrayToBytes(routeData.stops), serviceRouteId: routeData.serviceRouteId, }); }); return protoRoutesAdjacency; }; export const serializeServiceRoutesMap = ( serviceRoutes: ServiceRoute[], ): ProtoServiceRoute[] => { return serviceRoutes.map((value) => { return { type: serializeRouteType(value.type), name: value.name, routes: value.routes, }; }); }; export const deserializeStopsAdjacency = ( protoStopsAdjacency: ProtoStopAdjacency[], ): StopAdjacency[] => { const result: StopAdjacency[] = []; for (let i = 0; i < protoStopsAdjacency.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const value = protoStopsAdjacency[i]!; const transfers: Transfer[] = []; for (let j = 0; j < value.transfers.length; j++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const transfer = value.transfers[j]!; const newTransfer: Transfer = { destination: transfer.destination, type: parseTransferType(transfer.type), ...(transfer.minTransferTime !== undefined && { minTransferTime: transfer.minTransferTime, }), }; transfers.push(newTransfer); } const stopAdjacency: StopAdjacency = { routes: value.routes, }; if (transfers.length > 0) { stopAdjacency.transfers = transfers; } result.push(stopAdjacency); } return result; }; export const deserializeRoutesAdjacency = ( protoRoutesAdjacency: ProtoRoute[], ): Route[] => { const routesAdjacency: Route[] = []; for (let i = 0; i < protoRoutesAdjacency.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const value = protoRoutesAdjacency[i]!; const stops = bytesToUint32Array(value.stops); routesAdjacency.push( new Route( i, bytesToUint16Array(value.stopTimes), value.pickupDropOffTypes, stops, value.serviceRouteId, ), ); } return routesAdjacency; }; export const deserializeServiceRoutesMap = ( protoServiceRoutes: ProtoServiceRoute[], ): ServiceRoute[] => { const result: ServiceRoute[] = []; for (let i = 0; i < protoServiceRoutes.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const value = protoServiceRoutes[i]!; result.push({ type: parseRouteType(value.type), name: value.name, routes: value.routes, }); } return result; }; const parseTransferType = (type: ProtoTransferType): TransferType => { switch (type) { case ProtoTransferType.TRANSFER_TYPE_UNSPECIFIED: throw new Error('Unspecified protobuf transfer type.'); case ProtoTransferType.TRANSFER_TYPE_RECOMMENDED_TRANSFER_POINT: return TransferTypes.RECOMMENDED; case ProtoTransferType.TRANSFER_TYPE_TIMED_TRANSFER: return TransferTypes.GUARANTEED; case ProtoTransferType.TRANSFER_TYPE_REQUIRES_MINIMAL_TIME: return TransferTypes.REQUIRES_MINIMAL_TIME; case ProtoTransferType.TRANSFER_TYPE_IN_SEAT_TRANSFER: return TransferTypes.IN_SEAT; case ProtoTransferType.UNRECOGNIZED: throw new Error('Unrecognized protobuf transfer type.'); } }; const serializeTransferType = (type: TransferType): ProtoTransferType => { switch (type) { case TransferTypes.RECOMMENDED: return ProtoTransferType.TRANSFER_TYPE_RECOMMENDED_TRANSFER_POINT; case TransferTypes.GUARANTEED: return ProtoTransferType.TRANSFER_TYPE_TIMED_TRANSFER; case TransferTypes.REQUIRES_MINIMAL_TIME: return ProtoTransferType.TRANSFER_TYPE_REQUIRES_MINIMAL_TIME; case TransferTypes.IN_SEAT: return ProtoTransferType.TRANSFER_TYPE_IN_SEAT_TRANSFER; } }; const parseRouteType = (type: ProtoRouteType): RouteType => { switch (type) { case ProtoRouteType.ROUTE_TYPE_UNSPECIFIED: throw new Error('Unspecified protobuf route type.'); case ProtoRouteType.ROUTE_TYPE_TRAM: return RouteTypes.TRAM; case ProtoRouteType.ROUTE_TYPE_SUBWAY: return RouteTypes.SUBWAY; case ProtoRouteType.ROUTE_TYPE_RAIL: return RouteTypes.RAIL; case ProtoRouteType.ROUTE_TYPE_BUS: return RouteTypes.BUS; case ProtoRouteType.ROUTE_TYPE_FERRY: return RouteTypes.FERRY; case ProtoRouteType.ROUTE_TYPE_CABLE_TRAM: return RouteTypes.CABLE_TRAM; case ProtoRouteType.ROUTE_TYPE_AERIAL_LIFT: return RouteTypes.AERIAL_LIFT; case ProtoRouteType.ROUTE_TYPE_FUNICULAR: return RouteTypes.FUNICULAR; case ProtoRouteType.ROUTE_TYPE_TROLLEYBUS: return RouteTypes.TROLLEYBUS; case ProtoRouteType.ROUTE_TYPE_MONORAIL: return RouteTypes.MONORAIL; case ProtoRouteType.UNRECOGNIZED: default: throw new Error('Unrecognized protobuf route type.'); } }; const serializeRouteType = (type: RouteType): ProtoRouteType => { switch (type) { case RouteTypes.TRAM: return ProtoRouteType.ROUTE_TYPE_TRAM; case RouteTypes.SUBWAY: return ProtoRouteType.ROUTE_TYPE_SUBWAY; case RouteTypes.RAIL: return ProtoRouteType.ROUTE_TYPE_RAIL; case RouteTypes.BUS: return ProtoRouteType.ROUTE_TYPE_BUS; case RouteTypes.FERRY: return ProtoRouteType.ROUTE_TYPE_FERRY; case RouteTypes.CABLE_TRAM: return ProtoRouteType.ROUTE_TYPE_CABLE_TRAM; case RouteTypes.AERIAL_LIFT: return ProtoRouteType.ROUTE_TYPE_AERIAL_LIFT; case RouteTypes.FUNICULAR: return ProtoRouteType.ROUTE_TYPE_FUNICULAR; case RouteTypes.TROLLEYBUS: return ProtoRouteType.ROUTE_TYPE_TROLLEYBUS; case RouteTypes.MONORAIL: return ProtoRouteType.ROUTE_TYPE_MONORAIL; } }; export const serializeTripTransfers = ( tripTransfers: TripTransfers, ): ProtoTripTransferEntry[] => { const result: ProtoTripTransferEntry[] = []; for (const [tripBoardingId, destinations] of tripTransfers.entries()) { const [originStopIndex, originRouteId, originTripIndex] = decode(tripBoardingId); result.push({ origin: { stopIndex: originStopIndex, routeId: originRouteId, tripIndex: originTripIndex, }, destinations: destinations.map((tripStop) => ({ stopIndex: tripStop.stopIndex, routeId: tripStop.routeId, tripIndex: tripStop.tripIndex, })), }); } return result; }; export const deserializeTripTransfers = ( protoTripTransfer: ProtoTripTransferEntry[], ): TripTransfers => { const result = new Map<TripStopId, TripStop[]>(); for (let i = 0; i < protoTripTransfer.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const entry = protoTripTransfer[i]!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const origin = entry.origin!; const tripBoardingId = encode( origin.stopIndex, origin.routeId, origin.tripIndex, ); const destinations: TripStop[] = entry.destinations.map( (protoTripStop) => ({ stopIndex: protoTripStop.stopIndex, routeId: protoTripStop.routeId, tripIndex: protoTripStop.tripIndex, }), ); result.set(tripBoardingId, destinations); } return result; };