UNPKG

minotor

Version:

A lightweight client-side transit routing library.

292 lines (270 loc) 9.85 kB
import { SourceStopId, StopId } from '../stops/stops.js'; import { MUST_COORDINATE_WITH_DRIVER, MUST_PHONE_AGENCY, NOT_AVAILABLE, PickUpDropOffType, REGULAR, Route, RouteId, RoutesAdjacency, ServiceRouteId, ServiceRoutesMap, StopsAdjacency, } from '../timetable/timetable.js'; import { ServiceIds } from './services.js'; import { ParsedStopsMap } from './stops.js'; import { GtfsTime, toTime } from './time.js'; import { TransfersMap } from './transfers.js'; import { hashIds, parseCsv } from './utils.js'; export type TripId = string; export type TripIdsMap = Map<TripId, ServiceRouteId>; type TripEntry = { route_id: RouteId; service_id: ServiceRouteId; trip_id: TripId; }; export type GtfsPickupDropOffType = | '' // Not specified | 0 // Regularly scheduled | 1 // Not available | 2 // Must phone agency | 3; // Must coordinate with driver type StopTimeEntry = { trip_id: TripId; arrival_time?: GtfsTime; departure_time?: GtfsTime; stop_id: SourceStopId; stop_sequence: number; pickup_type?: GtfsPickupDropOffType; drop_off_type?: GtfsPickupDropOffType; }; /** * Parses the trips.txt file from a GTFS feed * * @param tripsStream The readable stream containing the trips data. * @param serviceIds A mapping of service IDs to corresponding route IDs. * @param routeIds A mapping of route IDs to route details. * @returns A mapping of trip IDs to corresponding route IDs. */ export const parseTrips = async ( tripsStream: NodeJS.ReadableStream, serviceIds: ServiceIds, routeIds: ServiceRoutesMap, ): Promise<TripIdsMap> => { const trips: TripIdsMap = new Map(); for await (const rawLine of parseCsv(tripsStream)) { const line = rawLine as TripEntry; if (!serviceIds.has(line.service_id)) { // The trip doesn't correspond to an active service continue; } if (!routeIds.get(line.route_id)) { // The trip doesn't correspond to a supported route continue; } trips.set(line.trip_id, line.route_id); } return trips; }; export const buildStopsAdjacencyStructure = ( validStops: Set<StopId>, routes: RoutesAdjacency, transfersMap: TransfersMap, ): StopsAdjacency => { const stopsAdjacency: StopsAdjacency = new Map(); for (const routeId of routes.keys()) { const route = routes.get(routeId) as Route; for (const stop of route.stops) { if (!stopsAdjacency.get(stop) && validStops.has(stop)) { stopsAdjacency.set(stop, { routes: [], transfers: [] }); } stopsAdjacency.get(stop)?.routes.push(routeId); } } for (const [stop, transfers] of transfersMap) { const s = stopsAdjacency.get(stop); if (s) { for (const transfer of transfers) { if (validStops.has(transfer.destination)) { s.transfers.push(transfer); } } } } return stopsAdjacency; }; /** * Parses the stop_times.txt data from a GTFS feed. * * @param stopTimesStream The readable stream containing the stop times data. * @param stopsMap A map of parsed stops from the GTFS feed. * @param validTripIds A map of valid trip IDs to corresponding route IDs. * @param validStopIds A set of valid stop IDs. * @returns A mapping of route IDs to route details. The routes returned correspond to the set of trips from GTFS that share the same stop list. */ export const parseStopTimes = async ( stopTimesStream: NodeJS.ReadableStream, stopsMap: ParsedStopsMap, validTripIds: TripIdsMap, validStopIds: Set<StopId>, ): Promise<RoutesAdjacency> => { /** * Inserts a trip at the right place in the routes adjacency structure. */ const addTrip = (currentTripId: TripId) => { const gtfsRouteId = validTripIds.get(currentTripId); if (!gtfsRouteId) { stops = []; arrivalTimes = []; departureTimes = []; pickUpTypes = []; dropOffTypes = []; return; } const routeId = `${gtfsRouteId}_${hashIds(stops)}`; let route = routes.get(routeId); if (!route) { const stopsCount = stops.length; const stopsArray = new Uint32Array(stops); const stopTimesArray = new Uint16Array(stopsCount * 2); for (let i = 0; i < stopsCount; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion stopTimesArray[i * 2] = arrivalTimes[i]!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion stopTimesArray[i * 2 + 1] = departureTimes[i]!; } const pickUpDropOffTypesArray = new Uint8Array(stopsCount * 2); for (let i = 0; i < stopsCount; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pickUpDropOffTypesArray[i * 2] = pickUpTypes[i]!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pickUpDropOffTypesArray[i * 2 + 1] = dropOffTypes[i]!; } route = { serviceRouteId: gtfsRouteId, stops: stopsArray, stopIndices: new Map(stops.map((stop, i) => [stop, i])), stopTimes: stopTimesArray, pickUpDropOffTypes: pickUpDropOffTypesArray, }; routes.set(routeId, route); for (const stop of stops) { validStopIds.add(stop); } } else { const tripFirstStopDeparture = departureTimes[0]; if (tripFirstStopDeparture === undefined) { throw new Error(`Empty trip ${currentTripId}`); } // Find the correct position to insert the new trip const stopsCount = stops.length; let insertPosition = 0; const existingTripsCount = route.stopTimes.length / (stopsCount * 2); for (let tripIndex = 0; tripIndex < existingTripsCount; tripIndex++) { const currentDeparture = route.stopTimes[tripIndex * stopsCount * 2 + 1]; if (currentDeparture && tripFirstStopDeparture > currentDeparture) { insertPosition = (tripIndex + 1) * stopsCount; } else { break; } } // insert data for the new trip at the right place const newStopTimesLength = route.stopTimes.length + stopsCount * 2; const newStopTimes = new Uint16Array(newStopTimesLength); const newPickUpDropOffTypes = new Uint8Array(newStopTimesLength); newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0); newPickUpDropOffTypes.set( route.pickUpDropOffTypes.slice(0, insertPosition * 2), 0, ); for (let i = 0; i < stopsCount; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion newStopTimes[(insertPosition + i) * 2] = arrivalTimes[i]!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion newStopTimes[(insertPosition + i) * 2 + 1] = departureTimes[i]!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion newPickUpDropOffTypes[(insertPosition + i) * 2] = pickUpTypes[i]!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion newPickUpDropOffTypes[(insertPosition + i) * 2 + 1] = dropOffTypes[i]!; } const afterInsertionSlice = route.stopTimes.slice(insertPosition * 2); newStopTimes.set(afterInsertionSlice, (insertPosition + stopsCount) * 2); const afterInsertionTypesSlice = route.pickUpDropOffTypes.slice( insertPosition * 2, ); newPickUpDropOffTypes.set( afterInsertionTypesSlice, (insertPosition + stopsCount) * 2, ); route.stopTimes = newStopTimes; route.pickUpDropOffTypes = newPickUpDropOffTypes; } stops = []; arrivalTimes = []; departureTimes = []; pickUpTypes = []; dropOffTypes = []; }; const routes: RoutesAdjacency = new Map(); let previousSeq = 0; let stops: StopId[] = []; let arrivalTimes: number[] = []; let departureTimes: number[] = []; let pickUpTypes: PickUpDropOffType[] = []; let dropOffTypes: PickUpDropOffType[] = []; let currentTripId: TripId | undefined = undefined; for await (const rawLine of parseCsv(stopTimesStream)) { const line = rawLine as StopTimeEntry; if (line.trip_id === currentTripId && line.stop_sequence <= previousSeq) { console.warn(`Stop sequences not increasing for trip ${line.trip_id}.`); continue; } if (!line.arrival_time && !line.departure_time) { console.warn( `Missing arrival or departure time for ${line.trip_id} at stop ${line.stop_id}.`, ); continue; } if (line.pickup_type === 1 && line.drop_off_type === 1) { continue; } if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) { addTrip(currentTripId); } currentTripId = line.trip_id; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion stops.push(stopsMap.get(line.stop_id + '')!.id); const departure = line.departure_time ?? line.arrival_time; const arrival = line.arrival_time ?? line.departure_time; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion arrivalTimes.push(toTime(arrival!).toMinutes()); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion departureTimes.push(toTime(departure!).toMinutes()); pickUpTypes.push(parsePickupDropOffType(line.pickup_type)); dropOffTypes.push(parsePickupDropOffType(line.drop_off_type)); previousSeq = line.stop_sequence; } if (currentTripId) { addTrip(currentTripId); } return routes; }; const parsePickupDropOffType = ( gtfsType?: GtfsPickupDropOffType, ): PickUpDropOffType => { switch (gtfsType) { default: console.warn(`Unknown pickup/drop-off type ${gtfsType}`); return REGULAR; case 0: return REGULAR; case 1: return NOT_AVAILABLE; case 2: return MUST_PHONE_AGENCY; case 3: return MUST_COORDINATE_WITH_DRIVER; } };