UNPKG

gtfs-to-blocks

Version:

Generate CSV of transit departure times organized by block_id in GTFS.

216 lines (182 loc) 6.02 kB
import { join } from 'node:path' import { writeFile } from 'node:fs/promises' import { sortBy } from 'lodash-es' import { openDb, importGtfs, getStoptimes, getDeadheadTimes, getRoutes, Config, ConfigAgency, Calendar, } from 'gtfs' import sanitize from 'sanitize-filename' import Timer from 'timer-machine' import { prepDirectory } from './file-utils.js' import { progressBar, log, logStats } from './log-utils.ts' import { fromGTFSTime, generateCSV, setDefaultConfig } from './utils.ts' import { formatTripSegments } from './formatters.js' import moment from 'moment' import untildify from 'untildify' const gtfsToBlocks = async (initialConfig: Config) => { const config = setDefaultConfig(initialConfig) const timer = new Timer() timer.start() const db = openDb({ sqlitePath: config.sqlitePath }) if (!config.agencies || config.agencies.length === 0) { throw new Error('No agencies defined in `config.json`') } if (!config.skipImport) { // Import GTFS await importGtfs(config) } const agencyKey = config.agencies .map((agency: ConfigAgency & { agency_key?: string }) => agency.agency_key) .join('-') const outputPath = config.outputPath ? untildify(config.outputPath) : join(process.cwd(), 'output', sanitize(agencyKey)) const outputStats = { trips: 0, tripSegments: 0, warnings: [], } as { trips: number tripSegments: number warnings: string[] } const calendars = db .prepare( 'SELECT DISTINCT service_id FROM calendar WHERE start_date <= ? AND end_date >= ?', ) .all([config.date, config.date]) if (calendars.length === 0) { throw new Error( `No calendars found for ${moment(config.date, 'YYYYMMDD').format( 'MMM D, YYYY', )}`, ) } const routes = getRoutes() const serviceIds = calendars.map((calendar: Calendar) => calendar.service_id) const trips = db .prepare( `SELECT trip_id, direction_id, service_id, block_id, route_id, trip_headsign FROM trips where service_id IN (${serviceIds .map(() => '?') .join(', ')})`, ) .all(serviceIds) const deadheads = config.includeDeadheads ? db .prepare( `SELECT deadhead_id, service_id, block_id FROM deadheads where service_id IN (${serviceIds .map(() => '?') .join(', ')})`, ) .all(serviceIds) : [] const tripSegments = [] const bar = progressBar( `${agencyKey}: Generating trip segments {bar} {value}/{total}`, trips.length, config, ) /* eslint-disable no-await-in-loop */ for (const trip of trips) { try { const stoptimes = getStoptimes( { trip_id: trip.trip_id }, [], [['stop_sequence', 'ASC']], ) for (const [index, stoptime] of stoptimes.entries()) { if (index < stoptimes.length - 1) { tripSegments.push({ blockId: trip.block_id, routeId: trip.route_id, route: routes.find((route) => route.route_id === trip.route_id), tripId: trip.trip_id, tripHeadsign: trip.trip_headsign, stopHeadsign: stoptime.stop_headsign, directionId: trip.direction_id, serviceId: trip.service_id, departureStopId: stoptime.stop_id, arrivalStopId: stoptimes[index + 1].stop_id, departureTime: stoptime.departure_time, arrivalTime: stoptimes[index + 1].arrival_time, isDeadhead: false, }) outputStats.tripSegments += 1 } } outputStats.trips += 1 bar?.increment() } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) outputStats.warnings.push(errorMessage) bar?.interrupt(errorMessage) } } for (const deadhead of deadheads) { try { const deadheadTimes = await getDeadheadTimes( { deadhead_id: deadhead.deadhead_id }, [], [['location_sequence', 'ASC']], ) for (const [index, deadheadTime] of deadheadTimes.entries()) { if (index < deadheadTimes.length - 1) { tripSegments.push({ blockId: deadhead.block_id, tripId: deadhead.deadhead_id, serviceId: deadhead.service_id, departureStopId: deadheadTime.ops_location_id ?? deadheadTime.stop_id, arrivalStopId: deadheadTimes[index + 1].ops_location_id ?? deadheadTimes[index + 1].stop_id, departureTime: deadheadTime.departure_time, arrivalTime: deadheadTimes[index + 1].arrival_time, isDeadhead: true, }) outputStats.tripSegments += 1 } } outputStats.trips += 1 bar?.increment() } catch (error: any) { const errorMessage = error instanceof Error ? error.message : String(error) outputStats.warnings.push(errorMessage) bar?.interrupt(errorMessage) } } /* eslint-enable no-await-in-loop */ const sortedTripSegments = sortBy(tripSegments, [ // Sort by integer else alphabetically (tripSegment) => parseInt(tripSegment.blockId, 10) || tripSegment.blockId, (tripSegment) => fromGTFSTime(tripSegment.departureTime), ]) const formattedTripSegments = formatTripSegments(sortedTripSegments, config) await prepDirectory(outputPath, config) config.assetPath = '../' const csv = await generateCSV(formattedTripSegments) const csvPath = join(outputPath, 'blocks.csv') await writeFile(csvPath, csv) // Print stats log(config)( `${agencyKey}: block export for ${moment(config.date, 'YYYYMMDD').format( 'MMM D, YYYY', )} created at ${csvPath}`, ) logStats(config)(outputStats) const seconds = Math.round(timer.time() / 1000) log(config)( `${agencyKey}: block export generation required ${seconds} seconds`, ) timer.stop() return csvPath } export default gtfsToBlocks