gtfs-bods
Version:
A CLI tool for processing UK Bus Open Data Service (BODS) GTFS data - import, export, and query transit data with ease
277 lines ⢠12.7 kB
JavaScript
import { Command } from 'commander';
import chalk from 'chalk';
import { importGtfs, exportGtfs } from 'gtfs';
import path from 'path';
import { access, constants } from 'fs/promises';
import { GTFSQueries } from './queries.js';
const program = new Command();
// Helper function to check if file exists
async function fileExists(filePath) {
try {
await access(filePath, constants.F_OK);
return true;
}
catch {
return false;
}
}
// Helper function to create config for import
function createImportConfig(zipFile, dbFile, agencyKey = 'gtfs-data') {
return {
agencies: [
{
agency_key: agencyKey,
path: path.resolve(zipFile)
}
],
sqlitePath: path.resolve(dbFile),
verbose: true,
logFunction: false
};
}
// Helper function to create config for export
function createExportConfig(dbFile, outputDir, agencyKey = 'gtfs-data') {
return {
agencies: [
{
agency_key: agencyKey
}
],
sqlitePath: path.resolve(dbFile),
exportPath: path.resolve(outputDir),
verbose: true
};
}
program
.name('gtfs-bods')
.description('CLI tool for processing UK Bus Open Data Service (BODS) GTFS data')
.version('1.0.0');
// Import command
program
.command('import')
.description('Import GTFS zip file into SQLite database')
.argument('<zip-file>', 'Path to GTFS zip file')
.argument('<db-file>', 'Path to output SQLite database file')
.option('-k, --agency-key <key>', 'Agency key identifier', 'gtfs-data')
.option('-v, --verbose', 'Enable verbose logging', true)
.action(async (zipFile, dbFile, options) => {
try {
console.log(chalk.blue('š Starting GTFS import process...'));
console.log(chalk.gray(`š Input: ${zipFile}`));
console.log(chalk.gray(`šļø Output: ${dbFile}`));
// Check if input file exists
if (!(await fileExists(zipFile))) {
console.error(chalk.red('ā Error: Input GTFS file not found:'), zipFile);
process.exit(1);
}
// Create import configuration
const config = createImportConfig(zipFile, dbFile, options.agencyKey);
console.log(chalk.yellow('š„ Importing GTFS data...'));
console.log(chalk.gray('ā³ This may take several minutes for large files...'));
const startTime = Date.now();
await importGtfs(config);
const endTime = Date.now();
const duration = Math.round((endTime - startTime) / 1000);
console.log(chalk.green('ā
Import completed successfully!'));
console.log(chalk.gray(`ā±ļø Import took ${duration} seconds`));
console.log(chalk.gray(`š Database saved to: ${path.resolve(dbFile)}`));
}
catch (error) {
console.error(chalk.red('ā Import failed:'), error);
process.exit(1);
}
});
// Export command
program
.command('export')
.description('Export SQLite database back to GTFS format')
.argument('<db-file>', 'Path to SQLite database file')
.argument('<output-dir>', 'Path to output directory for GTFS files')
.option('-k, --agency-key <key>', 'Agency key identifier', 'gtfs-data')
.option('-v, --verbose', 'Enable verbose logging', true)
.action(async (dbFile, outputDir, options) => {
try {
console.log(chalk.blue('š Starting GTFS export process...'));
console.log(chalk.gray(`šļø Input: ${dbFile}`));
console.log(chalk.gray(`š Output: ${outputDir}`));
// Check if database file exists
if (!(await fileExists(dbFile))) {
console.error(chalk.red('ā Error: Database file not found:'), dbFile);
process.exit(1);
}
// Create export configuration
const config = createExportConfig(dbFile, outputDir, options.agencyKey);
console.log(chalk.yellow('š¤ Exporting GTFS data...'));
const startTime = Date.now();
await exportGtfs(config);
const endTime = Date.now();
const duration = Math.round((endTime - startTime) / 1000);
console.log(chalk.green('ā
Export completed successfully!'));
console.log(chalk.gray(`ā±ļø Export took ${duration} seconds`));
console.log(chalk.gray(`š GTFS files saved to: ${path.resolve(outputDir)}`));
}
catch (error) {
console.error(chalk.red('ā Export failed:'), error);
process.exit(1);
}
});
// Query command
program
.command('query')
.description('Query GTFS database and display information')
.argument('<db-file>', 'Path to SQLite database file')
.option('-r, --report', 'Generate comprehensive report', false)
.option('-a, --agencies', 'List all agencies', false)
.option('-R, --routes', 'List all routes', false)
.option('-s, --stops', 'List all stops', false)
.option('-k, --agency-key <key>', 'Filter by agency key')
.option('--route-id <id>', 'Get details for specific route')
.option('--area <bounds>', 'Find stops in area (format: minLat,minLon,maxLat,maxLon)')
.action(async (dbFile, options) => {
try {
console.log(chalk.blue('š Querying GTFS database...'));
// Check if database file exists
if (!(await fileExists(dbFile))) {
console.error(chalk.red('ā Error: Database file not found:'), dbFile);
process.exit(1);
}
// Note: The gtfs library automatically uses the database from the last import/export operation
// For a CLI tool, we would need to implement a way to switch databases or reimport temporarily
if (options.report) {
console.log(chalk.yellow('š Generating comprehensive report...'));
await GTFSQueries.generateReport(options.agencyKey);
}
else if (options.agencies) {
const agencies = await GTFSQueries.getAllAgencies();
console.log(chalk.green(`š¢ Found ${agencies.length} agencies:`));
agencies.forEach((agency, index) => {
console.log(` ${index + 1}. ${chalk.bold(agency.agency_name)} (${agency.agency_id})`);
if (agency.agency_url)
console.log(` š ${agency.agency_url}`);
if (agency.agency_timezone)
console.log(` š ${agency.agency_timezone}`);
});
}
else if (options.routes) {
const routes = await GTFSQueries.getAllRoutes({ agencyKey: options.agencyKey });
console.log(chalk.green(`š Found ${routes.length} routes:`));
routes.slice(0, 20).forEach((route, index) => {
console.log(` ${index + 1}. ${chalk.bold(route.route_short_name || route.route_id)}: ${route.route_long_name || 'No description'}`);
});
if (routes.length > 20) {
console.log(chalk.gray(` ... and ${routes.length - 20} more routes`));
}
}
else if (options.stops) {
const stops = await GTFSQueries.getAllStops({ agencyKey: options.agencyKey });
console.log(chalk.green(`š Found ${stops.length} stops:`));
stops.slice(0, 20).forEach((stop, index) => {
console.log(` ${index + 1}. ${chalk.bold(stop.stop_name)} (${stop.stop_id})`);
console.log(` š ${stop.stop_lat}, ${stop.stop_lon}`);
});
if (stops.length > 20) {
console.log(chalk.gray(` ... and ${stops.length - 20} more stops`));
}
}
else if (options.routeId) {
const trips = await GTFSQueries.getTripsForRoute(options.routeId, options.agencyKey);
console.log(chalk.green(`š Found ${trips.length} trips for route ${options.routeId}:`));
trips.slice(0, 10).forEach((trip, index) => {
console.log(` ${index + 1}. Trip ${trip.trip_id} - ${trip.trip_headsign || 'No headsign'}`);
});
}
else if (options.area) {
const bounds = options.area.split(',').map((n) => parseFloat(n));
if (bounds.length !== 4) {
console.error(chalk.red('ā Error: Area bounds must be in format minLat,minLon,maxLat,maxLon'));
process.exit(1);
}
const [minLat, minLon, maxLat, maxLon] = bounds;
const stops = await GTFSQueries.findStopsInArea(minLat, minLon, maxLat, maxLon, options.agencyKey);
console.log(chalk.green(`š Found ${stops.length} stops in area:`));
stops.forEach((stop, index) => {
console.log(` ${index + 1}. ${stop.stop_name} - ${stop.stop_lat}, ${stop.stop_lon}`);
});
}
else {
// Default: show basic stats
const agencies = await GTFSQueries.getAllAgencies();
const routes = await GTFSQueries.getAllRoutes({ agencyKey: options.agencyKey });
const stops = await GTFSQueries.getAllStops({ agencyKey: options.agencyKey });
console.log(chalk.green('š Database Statistics:'));
console.log(` š¢ Agencies: ${chalk.bold(agencies.length)}`);
console.log(` š Routes: ${chalk.bold(routes.length)}`);
console.log(` š Stops: ${chalk.bold(stops.length)}`);
console.log('\n' + chalk.gray('Use --help to see more query options'));
}
}
catch (error) {
console.error(chalk.red('ā Query failed:'), error);
process.exit(1);
}
});
// Info command
program
.command('info')
.description('Display information about a GTFS file or database')
.argument('<file>', 'Path to GTFS zip file or SQLite database')
.action(async (file) => {
try {
if (!(await fileExists(file))) {
console.error(chalk.red('ā Error: File not found:'), file);
process.exit(1);
}
const ext = path.extname(file).toLowerCase();
if (ext === '.zip') {
console.log(chalk.blue('š¦ GTFS Zip File Information'));
console.log(chalk.gray('ā'.repeat(40)));
console.log(`š File: ${chalk.bold(path.basename(file))}`);
console.log(`š Path: ${path.resolve(file)}`);
// You could add zip file analysis here if needed
const { stat } = await import('fs/promises');
const stats = await stat(file);
console.log(`š Size: ${chalk.bold((stats.size / 1024 / 1024).toFixed(2))} MB`);
console.log(`š
Modified: ${chalk.bold(stats.mtime.toLocaleDateString())}`);
}
else if (ext === '.db' || ext === '.sqlite') {
console.log(chalk.blue('šļø SQLite Database Information'));
console.log(chalk.gray('ā'.repeat(40)));
console.log(`š File: ${chalk.bold(path.basename(file))}`);
console.log(`š Path: ${path.resolve(file)}`);
const { stat } = await import('fs/promises');
const stats = await stat(file);
console.log(`š Size: ${chalk.bold((stats.size / 1024 / 1024).toFixed(2))} MB`);
console.log(`š
Modified: ${chalk.bold(stats.mtime.toLocaleDateString())}`);
// Try to query basic stats
try {
// Note: For a fully functional CLI, you would need to configure the gtfs library
// to use the specific database file here
const agencies = await GTFSQueries.getAllAgencies();
const routes = await GTFSQueries.getAllRoutes();
const stops = await GTFSQueries.getAllStops();
console.log('\nš Database Content:');
console.log(` š¢ Agencies: ${chalk.bold(agencies.length)}`);
console.log(` š Routes: ${chalk.bold(routes.length)}`);
console.log(` š Stops: ${chalk.bold(stops.length)}`);
}
catch (error) {
console.log(chalk.yellow('\nā ļø Could not read database content (may not be a valid GTFS database)'));
}
}
else {
console.error(chalk.red('ā Error: Unsupported file type. Use .zip for GTFS files or .db/.sqlite for databases'));
process.exit(1);
}
}
catch (error) {
console.error(chalk.red('ā Info command failed:'), error);
process.exit(1);
}
});
// Error handling
program.configureOutput({
outputError: (str, write) => write(chalk.red(str))
});
program.parse();
//# sourceMappingURL=cli.js.map