UNPKG

minotor

Version:

A lightweight client-side transit routing library.

323 lines (280 loc) 9.29 kB
import { Duration } from './duration.js'; import { RoutesAdjacency as ProtoRoutesAdjacency, RouteType as ProtoRouteType, ServiceRoutesMap as ProtoServiceRoutesMap, StopsAdjacency as ProtoStopsAdjacency, Transfer as ProtoTransfer, TransferType as ProtoTransferType, } from './proto/timetable.js'; import { Route, RoutesAdjacency, RouteType, ServiceRoutesMap, StopsAdjacency, Transfer, TransferType, } from './timetable.js'; 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 function 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; } function 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; } function 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; } function 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: StopsAdjacency, ): ProtoStopsAdjacency => { const protoStopsAdjacency: ProtoStopsAdjacency = { stops: {}, }; stopsAdjacency.forEach( (value: { transfers: Transfer[]; routes: string[] }, key: number) => { protoStopsAdjacency.stops[key] = { transfers: value.transfers.map((transfer) => ({ destination: transfer.destination, type: serializeTransferType(transfer.type), ...(transfer.minTransferTime !== undefined && { minTransferTime: transfer.minTransferTime.toSeconds(), }), })), routes: value.routes, }; }, ); return protoStopsAdjacency; }; export const serializeRoutesAdjacency = ( routesAdjacency: RoutesAdjacency, ): ProtoRoutesAdjacency => { const protoRoutesAdjacency: ProtoRoutesAdjacency = { routes: {}, }; routesAdjacency.forEach((value: Route, key: string) => { protoRoutesAdjacency.routes[key] = { stopTimes: uint16ArrayToBytes(value.stopTimes), pickUpDropOffTypes: value.pickUpDropOffTypes, stops: uint32ArrayToBytes(value.stops), serviceRouteId: value.serviceRouteId, }; }); return protoRoutesAdjacency; }; export const serializeServiceRoutesMap = ( serviceRoutesMap: ServiceRoutesMap, ): ProtoServiceRoutesMap => { const protoServiceRoutesMap: ProtoServiceRoutesMap = { routes: {}, }; serviceRoutesMap.forEach( (value: { type: RouteType; name: string }, key: string) => { protoServiceRoutesMap.routes[key] = { type: serializeRouteType(value.type), name: value.name, }; }, ); return protoServiceRoutesMap; }; export const deserializeStopsAdjacency = ( protoStopsAdjacency: ProtoStopsAdjacency, ): StopsAdjacency => { const stopsAdjacency: StopsAdjacency = new Map(); Object.entries(protoStopsAdjacency.stops).forEach(([keyStr, value]) => { const key = parseInt(keyStr, 10); stopsAdjacency.set(key, { transfers: value.transfers.map( (transfer: ProtoTransfer): Transfer => ({ destination: transfer.destination, type: parseTransferType(transfer.type), ...(transfer.minTransferTime !== undefined && { minTransferTime: Duration.fromSeconds(transfer.minTransferTime), }), }), ), routes: value.routes, }); }); return stopsAdjacency; }; export const deserializeRoutesAdjacency = ( protoRoutesAdjacency: ProtoRoutesAdjacency, ): RoutesAdjacency => { const routesAdjacency: RoutesAdjacency = new Map(); Object.entries(protoRoutesAdjacency.routes).forEach(([key, value]) => { const stops = bytesToUint32Array(value.stops); const indices = new Map<number, number>(); for (let i = 0; i < stops.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion indices.set(stops[i]!, i); } routesAdjacency.set(key, { stopTimes: bytesToUint16Array(value.stopTimes), pickUpDropOffTypes: value.pickUpDropOffTypes, stops: stops, stopIndices: indices, serviceRouteId: value.serviceRouteId, }); }); return routesAdjacency; }; export const deserializeServiceRoutesMap = ( protoServiceRoutesMap: ProtoServiceRoutesMap, ): ServiceRoutesMap => { const serviceRoutesMap: ServiceRoutesMap = new Map(); Object.entries(protoServiceRoutesMap.routes).forEach(([key, value]) => { serviceRoutesMap.set(key, { type: parseRouteType(value.type), name: value.name, }); }); return serviceRoutesMap; }; const parseTransferType = (type: ProtoTransferType): TransferType => { switch (type) { case ProtoTransferType.RECOMMENDED_TRANSFER_POINT: return 'RECOMMENDED'; case ProtoTransferType.TIMED_TRANSFER: return 'GUARANTEED'; case ProtoTransferType.REQUIRES_MINIMAL_TIME: return 'REQUIRES_MINIMAL_TIME'; case ProtoTransferType.IN_SEAT_TRANSFER: return 'IN_SEAT'; case ProtoTransferType.UNRECOGNIZED: throw new Error('Unrecognized protobuf transfer type.'); } }; const serializeTransferType = (type: TransferType): ProtoTransferType => { switch (type) { case 'RECOMMENDED': return ProtoTransferType.RECOMMENDED_TRANSFER_POINT; case 'GUARANTEED': return ProtoTransferType.TIMED_TRANSFER; case 'REQUIRES_MINIMAL_TIME': return ProtoTransferType.REQUIRES_MINIMAL_TIME; case 'IN_SEAT': return ProtoTransferType.IN_SEAT_TRANSFER; } }; const parseRouteType = (type: ProtoRouteType): RouteType => { switch (type) { case ProtoRouteType.TRAM: return 'TRAM'; case ProtoRouteType.SUBWAY: return 'SUBWAY'; case ProtoRouteType.RAIL: return 'RAIL'; case ProtoRouteType.BUS: return 'BUS'; case ProtoRouteType.FERRY: return 'FERRY'; case ProtoRouteType.CABLE_TRAM: return 'CABLE_TRAM'; case ProtoRouteType.AERIAL_LIFT: return 'AERIAL_LIFT'; case ProtoRouteType.FUNICULAR: return 'FUNICULAR'; case ProtoRouteType.TROLLEYBUS: return 'TROLLEYBUS'; case ProtoRouteType.MONORAIL: return 'MONORAIL'; case ProtoRouteType.UNRECOGNIZED: default: throw new Error('Unrecognized protobuf route type.'); } }; const serializeRouteType = (type: RouteType): ProtoRouteType => { switch (type) { case 'TRAM': return ProtoRouteType.TRAM; case 'SUBWAY': return ProtoRouteType.SUBWAY; case 'RAIL': return ProtoRouteType.RAIL; case 'BUS': return ProtoRouteType.BUS; case 'FERRY': return ProtoRouteType.FERRY; case 'CABLE_TRAM': return ProtoRouteType.CABLE_TRAM; case 'AERIAL_LIFT': return ProtoRouteType.AERIAL_LIFT; case 'FUNICULAR': return ProtoRouteType.FUNICULAR; case 'TROLLEYBUS': return ProtoRouteType.TROLLEYBUS; case 'MONORAIL': return ProtoRouteType.MONORAIL; } };