minotor
Version:
A lightweight client-side transit routing library.
235 lines (217 loc) • 6.49 kB
text/typescript
import { Command } from 'commander';
import fs from 'fs';
import log from 'loglevel';
import { DateTime } from 'luxon';
import {
extendedGtfsProfile,
GtfsParser,
GtfsProfile,
standardGtfsProfile,
StraightLineTransferGenerator,
} from '../parser.js';
import { Router, StopsIndex, Timetable } from '../router.js';
import {
loadQueriesFromJson,
loadRangeQueriesFromJson,
prettyPrintPerformanceResults,
testBestRoutePerformance,
testRangeResultPerformance,
testRangeRouterPerformance,
testRouterPerformance,
} from './perf.js';
import { startRepl } from './repl.js';
const program = new Command();
export type GtfsVariant = string;
export type Config = {
[config: GtfsVariant]: GtfsProfile;
};
const profiles: Config = {
extended: extendedGtfsProfile,
standard: standardGtfsProfile,
};
program
.name('minotor')
.description('CLI for minotor route planner.')
.version('0.0.1');
program
.command('parse-gtfs')
.description('Parse a GTFS feed and output a timetable and stops file.')
.argument('<gtfsPath>', 'Path to GTFS data')
.option(
'-d, --date <date>',
'Date of the day to parse in yyyy-MM-dd format',
DateTime.now().toFormat('yyyy-MM-dd'),
)
.option(
'-t, --timetableOutputPath <path>',
'Path to output timetable file',
'/tmp/timetable',
)
.option(
'-s, --stopsOutputPath <path>',
'Path to output stops file',
'/tmp/stops',
)
.option(
'-p, --profileName <name>',
'Profile name for GTFS config',
'extended',
)
.option(
'--virtual-transfers-radius <meters>',
'Generate virtual walking transfers between stops within this radius (in meters). Disabled when omitted.',
)
.option('-v, --verbose', 'Verbose mode', false)
.action(
async (
gtfsPath: string,
options: {
date: string;
timetableOutputPath: string;
stopsOutputPath: string;
profileName: string;
virtualTransfersRadius?: string;
verbose: boolean;
},
) => {
if (options.verbose) {
log.setDefaultLevel(log.levels.INFO);
} else {
log.setDefaultLevel(log.levels.ERROR);
}
const radiusMeters =
options.virtualTransfersRadius !== undefined
? parseInt(options.virtualTransfersRadius, 10)
: undefined;
const transferGenerator =
radiusMeters !== undefined && Number.isFinite(radiusMeters)
? new StraightLineTransferGenerator({
maxDistanceMeters: radiusMeters,
})
: undefined;
const parser = new GtfsParser(
gtfsPath,
profiles[options.profileName],
transferGenerator,
);
const stopsIndex = await parser.parseStops();
fs.writeFileSync(options.stopsOutputPath, stopsIndex.serialize());
const timetable = await parser.parseTimetable(new Date(options.date));
fs.writeFileSync(options.timetableOutputPath, timetable.serialize());
},
);
program
.command('parse-stops')
.description('Parse a GTFS feed and output a stops file.')
.argument('<gtfsPath>', 'Path to GTFS data')
.option('-s, --outputPath <path>', 'Path to output stops file', '/tmp/stops')
.option('-p, --profileName <name>', 'Profile name for GTFS config', 'CH')
.option('-v, --verbose', 'Verbose mode', false)
.action(
async (
gtfsPath: string,
options: {
stopsOutputPath: string;
profileName: string;
verbose: boolean;
},
) => {
if (options.verbose) {
log.setDefaultLevel(log.levels.INFO);
} else {
log.setDefaultLevel(log.levels.ERROR);
}
const parser = new GtfsParser(gtfsPath, profiles[options.profileName]);
const stopsIndex = await parser.parseStops();
fs.writeFileSync(options.stopsOutputPath, stopsIndex.serialize());
},
);
program
.command('repl')
.description('Find stops matching a textual query')
.option('-s, --stopsPath <path>', 'Path to the stops file', '/tmp/stops')
.option(
'-t, --timetablePath <path>',
'Path to the timetable file',
'/tmp/timetable',
)
.action((options: { stopsPath: string; timetablePath: string }) => {
startRepl(options.stopsPath, options.timetablePath);
});
program
.command('perf')
.description('Evaluate the performance of the router on a set of routes.')
.argument('<routesPath>', 'Path to the JSON file containing the routes')
.option('-s, --stopsPath <path>', 'Path to the stops file', '/tmp/stops')
.option(
'-t, --timetablePath <path>',
'Path to the timetable file',
'/tmp/timetable',
)
.option(
'-i, --iterations <number>',
'Number of iterations for performance tests',
'20',
)
.action(
(
routesPath: string,
options: {
stopsPath: string;
timetablePath: string;
iterations: string;
},
) => {
const stopsIndex = StopsIndex.fromData(
fs.readFileSync(options.stopsPath),
);
const timetable = Timetable.fromData(
fs.readFileSync(options.timetablePath),
);
const router = new Router(timetable, stopsIndex);
const queries = loadQueriesFromJson(routesPath, stopsIndex);
const iterations = parseInt(options.iterations, 10);
const routerResults = testRouterPerformance(
router,
queries,
iterations,
stopsIndex,
);
prettyPrintPerformanceResults(
routerResults,
'Point queries — router.route()',
);
const bestRouteResults = testBestRoutePerformance(
router,
queries,
iterations,
stopsIndex,
);
prettyPrintPerformanceResults(
bestRouteResults,
'Point queries — result.bestRoute() (reconstruction only)',
);
const rangeQueries = loadRangeQueriesFromJson(routesPath, stopsIndex);
const rangeRouterResults = testRangeRouterPerformance(
router,
rangeQueries,
iterations,
stopsIndex,
);
prettyPrintPerformanceResults(
rangeRouterResults,
'Range queries — router.rangeRoute()',
);
const rangeResultResults = testRangeResultPerformance(
router,
rangeQueries,
iterations,
stopsIndex,
);
prettyPrintPerformanceResults(
rangeResultResults,
'Range queries — rangeResult.getRoutes() (reconstruction only)',
);
},
);
program.parse(process.argv);