UNPKG

minotor

Version:

A lightweight client-side transit routing library.

309 lines (281 loc) 10.9 kB
import log from 'loglevel'; import { DateTime } from 'luxon'; import StreamZip from 'node-stream-zip'; import { StopId } from '../stops/stops.js'; import { StopsIndex } from '../stops/stopsIndex.js'; import { RouteType, Timetable } from '../timetable/timetable.js'; import { TransferGenerator } from '../transfers/generator.js'; import { FrequenciesMap, parseFrequencies } from './frequencies.js'; import { standardGtfsProfile } from './profiles/standard.js'; import { indexRoutes, parseRoutes } from './routes.js'; import { parseCalendar, parseCalendarDates, ServiceIds } from './services.js'; import { parseStops } from './stops.js'; import { addGeneratedTransfers, addMissingSiblingTransfers, buildTripTransfers, ForbiddenTransfersMap, GtfsTripTransfer, parseTransfers, TransfersMap, } from './transfers.js'; import { buildStopsAdjacencyStructure, parseStopTimes, parseTrips, } from './trips.js'; import { Maybe } from './utils.js'; const CALENDAR_FILE = 'calendar.txt'; const CALENDAR_DATES_FILE = 'calendar_dates.txt'; const FREQUENCIES_FILE = 'frequencies.txt'; const ROUTES_FILE = 'routes.txt'; const TRIPS_FILE = 'trips.txt'; const STOP_TIMES_FILE = 'stop_times.txt'; const STOPS_FILE = 'stops.txt'; const TRANSFERS_FILE = 'transfers.txt'; export type GtfsProfile = { routeTypeParser: (routeType: number) => Maybe<RouteType>; /** * Derive fallback transfers between active stops that share a parent station. * * @default true */ deriveSiblingTransfers?: boolean; }; export class GtfsParser { private path: string; private profile: GtfsProfile; private transferGenerator?: TransferGenerator; constructor( path: string, profile: GtfsProfile = standardGtfsProfile, transferGenerator?: TransferGenerator, ) { // TODO: support input from multiple sources this.path = path; this.profile = profile; this.transferGenerator = transferGenerator; } /** * Parses a GTFS feed to extract all the data relevant to a given day in a transit-planner friendly format. * * @param date The active date. * @returns The parsed timetable. */ async parseTimetable(date: Date): Promise<Timetable> { log.setLevel('INFO'); const zip = new StreamZip.async({ file: this.path }); const entries = await zip.entries(); const datetime = DateTime.fromJSDate(date); const activeServiceIds: ServiceIds = new Set(); const activeStopIds = new Set<StopId>(); log.info(`Parsing ${STOPS_FILE}`); const stopsStart = performance.now(); const stopsStream = await zip.stream(STOPS_FILE); const parsedStops = await parseStops(stopsStream); const stopsEnd = performance.now(); log.info( `${parsedStops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`, ); if (entries[CALENDAR_FILE]) { log.info(`Parsing ${CALENDAR_FILE}`); const calendarStart = performance.now(); const calendarStream = await zip.stream(CALENDAR_FILE); await parseCalendar(calendarStream, activeServiceIds, datetime); const calendarEnd = performance.now(); log.info( `${activeServiceIds.size} valid services. (${(calendarEnd - calendarStart).toFixed(2)}ms)`, ); } if (entries[CALENDAR_DATES_FILE]) { log.info(`Parsing ${CALENDAR_DATES_FILE}`); const calendarDatesStart = performance.now(); const calendarDatesStream = await zip.stream(CALENDAR_DATES_FILE); await parseCalendarDates(calendarDatesStream, activeServiceIds, datetime); const calendarDatesEnd = performance.now(); log.info( `${activeServiceIds.size} valid services. (${(calendarDatesEnd - calendarDatesStart).toFixed(2)}ms)`, ); } log.info(`Parsing ${ROUTES_FILE}`); const routesStart = performance.now(); const routesStream = await zip.stream(ROUTES_FILE); const validGtfsRoutes = await parseRoutes(routesStream, this.profile); const routesEnd = performance.now(); log.info( `${validGtfsRoutes.size} valid GTFS routes. (${(routesEnd - routesStart).toFixed(2)}ms)`, ); log.info(`Parsing ${TRIPS_FILE}`); const tripsStart = performance.now(); const tripsStream = await zip.stream(TRIPS_FILE); const trips = await parseTrips( tripsStream, activeServiceIds, validGtfsRoutes, ); const tripsEnd = performance.now(); log.info( `${trips.size} valid trips. (${(tripsEnd - tripsStart).toFixed(2)}ms)`, ); let transfers: TransfersMap = new Map(); let forbiddenTransfers: ForbiddenTransfersMap = new Map(); let tripContinuationsList: GtfsTripTransfer[] = []; let guaranteedTripTransfersList: GtfsTripTransfer[] = []; if (entries[TRANSFERS_FILE]) { log.info(`Parsing ${TRANSFERS_FILE}`); const transfersStart = performance.now(); const transfersStream = await zip.stream(TRANSFERS_FILE); const { transfers: parsedTransfers, forbiddenTransfers: parsedForbiddenTransfers, tripContinuations: parsedTripContinuations, guaranteedTripTransfers: parsedGuaranteedTripTransfers, } = await parseTransfers(transfersStream, parsedStops, activeServiceIds); transfers = parsedTransfers; forbiddenTransfers = parsedForbiddenTransfers; tripContinuationsList = parsedTripContinuations; guaranteedTripTransfersList = parsedGuaranteedTripTransfers; const transfersEnd = performance.now(); log.info( `${transfers.size} valid transfers and ${tripContinuationsList.length} trip continuations and ${guaranteedTripTransfersList.length} guaranteed trip transfers. (${(transfersEnd - transfersStart).toFixed(2)}ms)`, ); } let frequenciesMap: FrequenciesMap | undefined; if (entries[FREQUENCIES_FILE]) { log.info(`Parsing ${FREQUENCIES_FILE}`); const frequenciesStart = performance.now(); const frequenciesStream = await zip.stream(FREQUENCIES_FILE); frequenciesMap = await parseFrequencies( frequenciesStream, new Set(trips.keys()), ); const frequenciesEnd = performance.now(); log.info( `${frequenciesMap.size} trips with frequency data. (${(frequenciesEnd - frequenciesStart).toFixed(2)}ms)`, ); } log.info(`Parsing ${STOP_TIMES_FILE}`); const stopTimesStart = performance.now(); const stopTimesStream = await zip.stream(STOP_TIMES_FILE); const { routes, serviceRoutesMap, tripsMapping } = await parseStopTimes( stopTimesStream, parsedStops, trips, activeStopIds, frequenciesMap, ); const serviceRoutes = indexRoutes(validGtfsRoutes, serviceRoutesMap); const stopTimesEnd = performance.now(); log.info( `${routes.length} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`, ); if (this.profile.deriveSiblingTransfers !== false) { const siblingTransfersStart = performance.now(); const siblingTransfersAdded = addMissingSiblingTransfers( parsedStops, activeStopIds, transfers, forbiddenTransfers, ); const siblingTransfersEnd = performance.now(); log.info( `${siblingTransfersAdded} sibling transfers added. (${(siblingTransfersEnd - siblingTransfersStart).toFixed(2)}ms)`, ); } if (this.transferGenerator) { log.info('Generating virtual transfers'); const virtualTransfersStart = performance.now(); // Only route-served stops with coordinates are worth connecting: a // transfer to a stop no route calls at is a dead end for the router. const originStops = Array.from(parsedStops.values()).filter( (stop) => activeStopIds.has(stop.id) && stop.lat !== undefined && stop.lon !== undefined, ); const stopsIndex = new StopsIndex(Array.from(parsedStops.values())); const generatedTransfers = await this.transferGenerator.generate( originStops, stopsIndex, ); const addedTransfers = addGeneratedTransfers( generatedTransfers, activeStopIds, transfers, forbiddenTransfers, ); const virtualTransfersEnd = performance.now(); log.info( `${addedTransfers} virtual transfers added. (${(virtualTransfersEnd - virtualTransfersStart).toFixed(2)}ms)`, ); } log.info('Building stops adjacency structure'); const stopsAdjacencyStart = performance.now(); const stopsAdjacency = buildStopsAdjacencyStructure( serviceRoutes, routes, transfers, parsedStops.size, activeStopIds, ); const stopsAdjacencyEnd = performance.now(); log.info( `${stopsAdjacency.length} valid stops in the structure. (${(stopsAdjacencyEnd - stopsAdjacencyStart).toFixed(2)}ms)`, ); await zip.close(); // temporary timetable for building continuations const timetable = new Timetable(stopsAdjacency, routes, serviceRoutes); log.info('Building in-seat trip continuations'); const tripContinuationsStart = performance.now(); const tripContinuations = buildTripTransfers( tripsMapping, tripContinuationsList, timetable, activeStopIds, ); const tripContinuationsEnd = performance.now(); log.info( `${tripContinuations.size} in-seat trip continuations origins created. (${(tripContinuationsEnd - tripContinuationsStart).toFixed(2)}ms)`, ); log.info('Parsing complete.'); log.info('Building guaranteed trip transfers'); const guaranteedTripTransfersStart = performance.now(); const guaranteedTripTransfers = buildTripTransfers( tripsMapping, guaranteedTripTransfersList, timetable, activeStopIds, ); const guaranteedTripTransfersEnd = performance.now(); log.info( `${guaranteedTripTransfers.size} guaranteed trip transfers origins created. (${(guaranteedTripTransfersEnd - guaranteedTripTransfersStart).toFixed(2)}ms)`, ); log.info('Parsing complete.'); return new Timetable( stopsAdjacency, routes, serviceRoutes, tripContinuations, guaranteedTripTransfers, ); } /** * Parses a GTFS feed to extract all stops. * * @param activeStops The set of active stop IDs to include in the index. * @returns An index of stops. */ async parseStops(): Promise<StopsIndex> { const zip = new StreamZip.async({ file: this.path }); log.info(`Parsing ${STOPS_FILE}`); const stopsStart = performance.now(); const stopsStream = await zip.stream(STOPS_FILE); const stops = await parseStops(stopsStream); const stopsEnd = performance.now(); log.info( `${stops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`, ); await zip.close(); return new StopsIndex(Array.from(stops.values())); } }