minotor
Version:
A lightweight client-side transit routing library.
323 lines (280 loc) • 9.29 kB
text/typescript
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;
}
};