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