transit-departures-widget 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app/index.js
CHANGED
|
@@ -403,8 +403,8 @@ function generateTransitDeparturesWidgetJson(config2) {
|
|
|
403
403
|
);
|
|
404
404
|
const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
|
|
405
405
|
return {
|
|
406
|
-
routes: removeNulls(sortedRoutes),
|
|
407
|
-
stops: removeNulls(sortedStops)
|
|
406
|
+
routes: arrayOfArrays(removeNulls(sortedRoutes)),
|
|
407
|
+
stops: arrayOfArrays(removeNulls(sortedStops))
|
|
408
408
|
};
|
|
409
409
|
}
|
|
410
410
|
function removeNulls(data) {
|
|
@@ -425,6 +425,15 @@ function removeNulls(data) {
|
|
|
425
425
|
return data;
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
|
+
function arrayOfArrays(array) {
|
|
429
|
+
if (array.length === 0) {
|
|
430
|
+
return { fields: [], rows: [] };
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
fields: Object.keys(array[0]),
|
|
434
|
+
rows: array.map((item) => Object.values(item))
|
|
435
|
+
};
|
|
436
|
+
}
|
|
428
437
|
function formatWhereClause(key, value) {
|
|
429
438
|
if (Array.isArray(value)) {
|
|
430
439
|
let whereClause = `${sqlString.escapeId(key)} IN (${value.filter((v) => v !== null).map((v) => sqlString.escape(v)).join(", ")})`;
|
package/dist/app/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/app/index.ts","../../src/lib/file-utils.ts","../../src/lib/utils.ts","../../src/lib/logging/log.ts","../../src/lib/logging/messages.ts","../../src/lib/config/defaults.ts"],"sourcesContent":["import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { openDb, importGtfs, type ConfigAgency } from 'gtfs'\nimport express from 'express'\nimport { clone, omit } from 'lodash-es'\nimport untildify from 'untildify'\n\nimport { getPathToViewsFolder } from '../lib/file-utils.js'\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.js'\n\nconst argv = yargs(hideBin(process.argv))\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .parseSync()\n\nconst app = express()\n\nconst configPath =\n (argv.configPath as string) || join(process.cwd(), 'config.json')\nconst selectedConfig = JSON.parse(readFileSync(configPath, 'utf8'))\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.logFunction = console.log\n\ntry {\n openDb(config)\n\n // Import GTFS\n const gtfsPath = config.agency.gtfs_static_path\n const gtfsUrl = config.agency.gtfs_static_url\n\n if (!gtfsPath && !gtfsUrl) {\n throw new Error(\n 'Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json.',\n )\n }\n\n const agencyImportConfig: ConfigAgency = gtfsPath\n ? { path: gtfsPath }\n : { url: gtfsUrl as string }\n\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [agencyImportConfig],\n }\n\n await importGtfs(gtfsImportConfig)\n} catch (error: any) {\n console.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists and run gtfs-to-html to import GTFS before running this app.`,\n )\n throw error\n}\n\napp.set('views', getPathToViewsFolder(config))\napp.set('view engine', 'pug')\n\n// Logging middleware\napp.use((req, res, next) => {\n console.log(`${req.method} ${req.url}`)\n next()\n})\n\n// Serve static assets\nconst staticAssetPath =\n config.templatePath === undefined\n ? getPathToViewsFolder(config)\n : untildify(config.templatePath)\n\napp.use(express.static(staticAssetPath))\n\nconst frontendLibraryPaths = [\n { route: '/js', package: 'pbf', subPath: 'dist' },\n { route: '/js', package: 'gtfs-realtime-pbf-js-module', subPath: '' },\n { route: '/js', package: 'accessible-autocomplete', subPath: '' },\n { route: '/css', package: 'accessible-autocomplete', subPath: '' },\n]\n\n// Helper function to resolve package path\nconst resolvePackagePath = (packageName: string, subPath: string): string => {\n const packagePath = dirname(fileURLToPath(import.meta.resolve(packageName)))\n return subPath ? join(packagePath, subPath) : packagePath\n}\n\n// Register all frontend library package routes\nfor (const { route, package: pkg, subPath } of frontendLibraryPaths) {\n app.use(route, express.static(resolvePackagePath(pkg, subPath)))\n}\n\n/*\n * Show the transit departures widget\n */\napp.get('/', async (request, response, next) => {\n try {\n const html = await generateTransitDeparturesWidgetHtml(config)\n response.send(html)\n } catch (error) {\n next(error)\n }\n})\n\n/*\n * Provide data\n */\napp.get('/data/routes.json', async (request, response, next) => {\n try {\n const { routes } = await generateTransitDeparturesWidgetJson(config)\n response.json(routes)\n } catch (error) {\n next(error)\n }\n})\n\napp.get('/data/stops.json', async (request, response, next) => {\n try {\n const { stops } = await generateTransitDeparturesWidgetJson(config)\n response.json(stops)\n } catch (error) {\n next(error)\n }\n})\n\n// Fallback 404 route\napp.use((req, res) => {\n res.status(404).send('Not Found')\n})\n\n// Error handling middleware\napp.use(\n (\n err: Error,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n console.error(err.stack)\n res.status(500).send('Something broke!')\n },\n)\n\nconst startServer = async (port: number): Promise<void> => {\n try {\n await new Promise<void>((resolve, reject) => {\n const server = app\n .listen(port)\n .once('listening', () => {\n console.log(`Express server listening on port ${port}`)\n resolve()\n })\n .once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n console.log(`Port ${port} is in use, trying ${port + 1}`)\n server.close()\n resolve(startServer(port + 1))\n } else {\n reject(err)\n }\n })\n })\n } catch (err) {\n console.error('Failed to start server:', err)\n process.exit(1)\n }\n}\n\nconst port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000\nstartServer(port)\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n access,\n cp,\n copyFile,\n mkdir,\n readdir,\n readFile,\n rm,\n} from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to this module's folder.\n */\nexport function getPathToThisModuleFolder() {\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to this module's folder\n let distFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n distFolderPath = resolve(__dirname, '../../')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n distFolderPath = resolve(__dirname, '../')\n } else {\n // In case it's neither, fallback to project root\n distFolderPath = resolve(__dirname, '../../')\n }\n\n return distFolderPath\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n return join(getPathToThisModuleFolder(), 'views/default')\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n const thisModuleFolderPath = getPathToThisModuleFolder()\n\n const foldersToCopy = ['css', 'js']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n\n // Copy js and css libraries from node_modules\n await copyFile(\n join(thisModuleFolderPath, 'dist/frontend_libraries/pbf.js'),\n join(outputPath, 'js/pbf.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/gtfs-realtime.browser.proto.js',\n ),\n join(outputPath, 'js/gtfs-realtime.browser.proto.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.js',\n ),\n join(outputPath, 'js/accessible-autocomplete.min.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.css',\n ),\n join(outputPath, 'css/accessible-autocomplete.min.css'),\n )\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport {\n ConfigWithI18n,\n GTFSCalendar,\n GTFSRoute,\n GTFSRouteDirection,\n GTFSStop,\n} from '../types/gtfs.ts'\nimport { createLogger } from './logging/log.ts'\nimport { messages } from './logging/messages.ts'\nexport { setDefaultConfig } from './config/defaults.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config): GTFSCalendar[] => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route: GTFSRoute) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: GTFSRoute, config: ConfigWithI18n) {\n const logger = createLogger(config)\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n .filter((direction) => direction.direction_id !== undefined)\n .map((direction) => ({\n direction_id: direction.direction_id as number | string,\n direction: direction.direction,\n }))\n\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsForRoute(route.route_id))\n return []\n }\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: GTFSCalendar) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: config.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n if (!longestTripStoptimes) {\n return []\n }\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\nfunction getStopsForDirection(\n route: GTFSRoute,\n direction: GTFSRouteDirection,\n config: Config,\n stopCache?: Map<string, GTFSStop>,\n) {\n const logger = createLogger(config)\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(\n messages.noActiveCalendarsForDirection(\n route.route_id,\n direction.direction_id,\n ),\n )\n return []\n }\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar: GTFSCalendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stopFields: (keyof GTFSStop)[] = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const missingStopIds = stopCache\n ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId))\n : deduplicatedStopIds\n\n const fetchedStops = missingStopIds.length\n ? (getStops as unknown as (query: any, fields?: any) => GTFSStop[])(\n { stop_id: missingStopIds },\n stopFields,\n )\n : []\n\n if (stopCache) {\n for (const stop of fetchedStops) {\n stopCache.set(stop.stop_id, stop)\n }\n }\n\n return deduplicatedStopIds\n .map((stopId: string) => {\n const stop =\n stopCache?.get(stopId) ??\n fetchedStops.find((candidate) => candidate.stop_id === stopId)\n\n if (!stop) {\n logger.warn(\n messages.stopNotFound(route.route_id, direction.direction_id, stopId),\n )\n }\n\n return stop\n })\n .filter(Boolean) as GTFSStop[]\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: ConfigWithI18n) {\n const templateVars = {\n config,\n __: config.__,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: ConfigWithI18n) {\n const logger = createLogger(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsGlobal)\n return { routes: [], stops: [] }\n }\n\n const routes = getRoutes() as GTFSRoute[]\n const stops: GTFSStop[] = []\n const filteredRoutes: GTFSRoute[] = []\n const stopCache = new Map<string, GTFSStop>()\n\n for (const route of routes) {\n const routeWithFullName: GTFSRoute = {\n ...route,\n route_full_name: formatRouteName(route),\n }\n\n const directions = getDirectionsForRoute(routeWithFullName, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logger.warn(messages.routeHasNoDirections(route.route_id))\n continue\n }\n\n const directionsWithData = directions\n .map((direction) => {\n const directionStops = getStopsForDirection(\n routeWithFullName,\n direction,\n config,\n stopCache,\n )\n\n if (directionStops.length === 0) {\n return null\n }\n\n stops.push(...directionStops)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: GTFSCalendar) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n\n return {\n ...direction,\n stopIds: directionStops.map((stop) => stop.stop_id),\n tripIds: trips.map((trip) => trip.trip_id),\n }\n })\n .filter(Boolean) as GTFSRouteDirection[]\n\n if (directionsWithData.length === 0) {\n continue\n }\n\n filteredRoutes.push({\n ...routeWithFullName,\n directions: directionsWithData,\n })\n }\n\n // Sort routes deterministically, handling mixed numeric/alphanumeric IDs\n const sortedRoutes = [...filteredRoutes].sort((a, b) => {\n const aShort = a.route_short_name ?? ''\n const bShort = b.route_short_name ?? ''\n const aNum = Number.parseInt(aShort, 10)\n const bNum = Number.parseInt(bShort, 10)\n\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {\n return aNum - bNum\n }\n\n if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {\n return 1\n }\n\n if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {\n return -1\n }\n\n return aShort.localeCompare(bShort, undefined, {\n numeric: true,\n sensitivity: 'base',\n })\n })\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => ({\n ...stop,\n is_parent_station: true,\n })),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (\n data !== null &&\n typeof data === 'object' &&\n Object.getPrototypeOf(data) === Object.prototype\n ) {\n return Object.entries(data).reduce<Record<string, unknown>>(\n (acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n },\n {},\n )\n } else {\n return data\n }\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../../types/global_interfaces.ts'\n\nexport type Logger = {\n info: (text: string, overwrite?: boolean) => void\n warn: (text: string) => void\n error: (text: string) => void\n}\n\nconst formatWarning = (text: string) => {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\nexport const formatError = (error: any) => {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n\nconst logInfo = (config: Config) => {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite?: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\nconst logWarn = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\nconst logError = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Create a structured logger with consistent methods.\n */\nexport function createLogger(config: Config): Logger {\n return {\n info: logInfo(config),\n warn: logWarn(config),\n error: logError(config),\n }\n}\n","export const messages = {\n noActiveCalendarsGlobal:\n 'No active calendars found for the configured date range - returning empty routes and stops',\n noActiveCalendarsForRoute: (routeId: string) =>\n `route_id ${routeId} has no active calendars in range - skipping directions`,\n noActiveCalendarsForDirection: (\n routeId: string,\n directionId: string | number,\n ) =>\n `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,\n routeHasNoDirections: (routeId: string) =>\n `route_id ${routeId} has no directions - skipping`,\n stopNotFound: (\n routeId: string,\n directionId: string | number,\n stopId: string,\n ) =>\n `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`,\n}\n","import { join } from 'path'\nimport { I18n } from 'i18n'\n\nimport { Config } from '../../types/global_interfaces.ts'\nimport { getPathToViewsFolder } from '../file-utils.ts'\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\n}\n"],"mappings":"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","names":["dirname","join","fileURLToPath","openDb","untildify","config","config","config","join","config","join","config","stoptimes","join","openDb","untildify","dirname","fileURLToPath","port","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../../src/app/index.ts","../../src/lib/file-utils.ts","../../src/lib/utils.ts","../../src/lib/logging/log.ts","../../src/lib/logging/messages.ts","../../src/lib/config/defaults.ts"],"sourcesContent":["import { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport { openDb, importGtfs, type ConfigAgency } from 'gtfs'\nimport express from 'express'\nimport { clone, omit } from 'lodash-es'\nimport untildify from 'untildify'\n\nimport { getPathToViewsFolder } from '../lib/file-utils.js'\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.js'\n\nconst argv = yargs(hideBin(process.argv))\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .parseSync()\n\nconst app = express()\n\nconst configPath =\n (argv.configPath as string) || join(process.cwd(), 'config.json')\nconst selectedConfig = JSON.parse(readFileSync(configPath, 'utf8'))\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.logFunction = console.log\n\ntry {\n openDb(config)\n\n // Import GTFS\n const gtfsPath = config.agency.gtfs_static_path\n const gtfsUrl = config.agency.gtfs_static_url\n\n if (!gtfsPath && !gtfsUrl) {\n throw new Error(\n 'Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json.',\n )\n }\n\n const agencyImportConfig: ConfigAgency = gtfsPath\n ? { path: gtfsPath }\n : { url: gtfsUrl as string }\n\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [agencyImportConfig],\n }\n\n await importGtfs(gtfsImportConfig)\n} catch (error: any) {\n console.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists and run gtfs-to-html to import GTFS before running this app.`,\n )\n throw error\n}\n\napp.set('views', getPathToViewsFolder(config))\napp.set('view engine', 'pug')\n\n// Logging middleware\napp.use((req, res, next) => {\n console.log(`${req.method} ${req.url}`)\n next()\n})\n\n// Serve static assets\nconst staticAssetPath =\n config.templatePath === undefined\n ? getPathToViewsFolder(config)\n : untildify(config.templatePath)\n\napp.use(express.static(staticAssetPath))\n\nconst frontendLibraryPaths = [\n { route: '/js', package: 'pbf', subPath: 'dist' },\n { route: '/js', package: 'gtfs-realtime-pbf-js-module', subPath: '' },\n { route: '/js', package: 'accessible-autocomplete', subPath: '' },\n { route: '/css', package: 'accessible-autocomplete', subPath: '' },\n]\n\n// Helper function to resolve package path\nconst resolvePackagePath = (packageName: string, subPath: string): string => {\n const packagePath = dirname(fileURLToPath(import.meta.resolve(packageName)))\n return subPath ? join(packagePath, subPath) : packagePath\n}\n\n// Register all frontend library package routes\nfor (const { route, package: pkg, subPath } of frontendLibraryPaths) {\n app.use(route, express.static(resolvePackagePath(pkg, subPath)))\n}\n\n/*\n * Show the transit departures widget\n */\napp.get('/', async (request, response, next) => {\n try {\n const html = await generateTransitDeparturesWidgetHtml(config)\n response.send(html)\n } catch (error) {\n next(error)\n }\n})\n\n/*\n * Provide data\n */\napp.get('/data/routes.json', async (request, response, next) => {\n try {\n const { routes } = await generateTransitDeparturesWidgetJson(config)\n response.json(routes)\n } catch (error) {\n next(error)\n }\n})\n\napp.get('/data/stops.json', async (request, response, next) => {\n try {\n const { stops } = await generateTransitDeparturesWidgetJson(config)\n response.json(stops)\n } catch (error) {\n next(error)\n }\n})\n\n// Fallback 404 route\napp.use((req, res) => {\n res.status(404).send('Not Found')\n})\n\n// Error handling middleware\napp.use(\n (\n err: Error,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n console.error(err.stack)\n res.status(500).send('Something broke!')\n },\n)\n\nconst startServer = async (port: number): Promise<void> => {\n try {\n await new Promise<void>((resolve, reject) => {\n const server = app\n .listen(port)\n .once('listening', () => {\n console.log(`Express server listening on port ${port}`)\n resolve()\n })\n .once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n console.log(`Port ${port} is in use, trying ${port + 1}`)\n server.close()\n resolve(startServer(port + 1))\n } else {\n reject(err)\n }\n })\n })\n } catch (err) {\n console.error('Failed to start server:', err)\n process.exit(1)\n }\n}\n\nconst port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000\nstartServer(port)\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n access,\n cp,\n copyFile,\n mkdir,\n readdir,\n readFile,\n rm,\n} from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to this module's folder.\n */\nexport function getPathToThisModuleFolder() {\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to this module's folder\n let distFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n distFolderPath = resolve(__dirname, '../../')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n distFolderPath = resolve(__dirname, '../')\n } else {\n // In case it's neither, fallback to project root\n distFolderPath = resolve(__dirname, '../../')\n }\n\n return distFolderPath\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n return join(getPathToThisModuleFolder(), 'views/default')\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n const thisModuleFolderPath = getPathToThisModuleFolder()\n\n const foldersToCopy = ['css', 'js']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n\n // Copy js and css libraries from node_modules\n await copyFile(\n join(thisModuleFolderPath, 'dist/frontend_libraries/pbf.js'),\n join(outputPath, 'js/pbf.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/gtfs-realtime.browser.proto.js',\n ),\n join(outputPath, 'js/gtfs-realtime.browser.proto.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.js',\n ),\n join(outputPath, 'js/accessible-autocomplete.min.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.css',\n ),\n join(outputPath, 'css/accessible-autocomplete.min.css'),\n )\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport {\n ConfigWithI18n,\n GTFSCalendar,\n GTFSRoute,\n GTFSRouteDirection,\n GTFSStop,\n} from '../types/gtfs.ts'\nimport { createLogger } from './logging/log.ts'\nimport { messages } from './logging/messages.ts'\nexport { setDefaultConfig } from './config/defaults.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config): GTFSCalendar[] => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route: GTFSRoute) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: GTFSRoute, config: ConfigWithI18n) {\n const logger = createLogger(config)\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n .filter((direction) => direction.direction_id !== undefined)\n .map((direction) => ({\n direction_id: direction.direction_id as number | string,\n direction: direction.direction,\n }))\n\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsForRoute(route.route_id))\n return []\n }\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: GTFSCalendar) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: config.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n if (!longestTripStoptimes) {\n return []\n }\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\nfunction getStopsForDirection(\n route: GTFSRoute,\n direction: GTFSRouteDirection,\n config: Config,\n stopCache?: Map<string, GTFSStop>,\n) {\n const logger = createLogger(config)\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(\n messages.noActiveCalendarsForDirection(\n route.route_id,\n direction.direction_id,\n ),\n )\n return []\n }\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar: GTFSCalendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stopFields: (keyof GTFSStop)[] = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const missingStopIds = stopCache\n ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId))\n : deduplicatedStopIds\n\n const fetchedStops = missingStopIds.length\n ? (getStops as unknown as (query: any, fields?: any) => GTFSStop[])(\n { stop_id: missingStopIds },\n stopFields,\n )\n : []\n\n if (stopCache) {\n for (const stop of fetchedStops) {\n stopCache.set(stop.stop_id, stop)\n }\n }\n\n return deduplicatedStopIds\n .map((stopId: string) => {\n const stop =\n stopCache?.get(stopId) ??\n fetchedStops.find((candidate) => candidate.stop_id === stopId)\n\n if (!stop) {\n logger.warn(\n messages.stopNotFound(route.route_id, direction.direction_id, stopId),\n )\n }\n\n return stop\n })\n .filter(Boolean) as GTFSStop[]\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: ConfigWithI18n) {\n const templateVars = {\n config,\n __: config.__,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: ConfigWithI18n) {\n const logger = createLogger(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsGlobal)\n return { routes: [], stops: [] }\n }\n\n const routes = getRoutes() as GTFSRoute[]\n const stops: GTFSStop[] = []\n const filteredRoutes: GTFSRoute[] = []\n const stopCache = new Map<string, GTFSStop>()\n\n for (const route of routes) {\n const routeWithFullName: GTFSRoute = {\n ...route,\n route_full_name: formatRouteName(route),\n }\n\n const directions = getDirectionsForRoute(routeWithFullName, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logger.warn(messages.routeHasNoDirections(route.route_id))\n continue\n }\n\n const directionsWithData = directions\n .map((direction) => {\n const directionStops = getStopsForDirection(\n routeWithFullName,\n direction,\n config,\n stopCache,\n )\n\n if (directionStops.length === 0) {\n return null\n }\n\n stops.push(...directionStops)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: GTFSCalendar) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n\n return {\n ...direction,\n stopIds: directionStops.map((stop) => stop.stop_id),\n tripIds: trips.map((trip) => trip.trip_id),\n }\n })\n .filter(Boolean) as GTFSRouteDirection[]\n\n if (directionsWithData.length === 0) {\n continue\n }\n\n filteredRoutes.push({\n ...routeWithFullName,\n directions: directionsWithData,\n })\n }\n\n // Sort routes deterministically, handling mixed numeric/alphanumeric IDs\n const sortedRoutes = [...filteredRoutes].sort((a, b) => {\n const aShort = a.route_short_name ?? ''\n const bShort = b.route_short_name ?? ''\n const aNum = Number.parseInt(aShort, 10)\n const bNum = Number.parseInt(bShort, 10)\n\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {\n return aNum - bNum\n }\n\n if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {\n return 1\n }\n\n if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {\n return -1\n }\n\n return aShort.localeCompare(bShort, undefined, {\n numeric: true,\n sensitivity: 'base',\n })\n })\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => ({\n ...stop,\n is_parent_station: true,\n })),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: arrayOfArrays(removeNulls(sortedRoutes)),\n stops: arrayOfArrays(removeNulls(sortedStops)),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (\n data !== null &&\n typeof data === 'object' &&\n Object.getPrototypeOf(data) === Object.prototype\n ) {\n return Object.entries(data).reduce<Record<string, unknown>>(\n (acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n },\n {},\n )\n } else {\n return data\n }\n}\n\n/*\n * Convert an array of objects into an Array-of-arrays JSON: { \"fields\": [...], \"rows\": [[...], ...] }\n */\nfunction arrayOfArrays(array: any[]): { fields: string[]; rows: any[][] } {\n if (array.length === 0) {\n return { fields: [], rows: [] }\n }\n\n return {\n fields: Object.keys(array[0]),\n rows: array.map((item) => Object.values(item)),\n }\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../../types/global_interfaces.ts'\n\nexport type Logger = {\n info: (text: string, overwrite?: boolean) => void\n warn: (text: string) => void\n error: (text: string) => void\n}\n\nconst formatWarning = (text: string) => {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\nexport const formatError = (error: any) => {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n\nconst logInfo = (config: Config) => {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite?: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\nconst logWarn = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\nconst logError = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Create a structured logger with consistent methods.\n */\nexport function createLogger(config: Config): Logger {\n return {\n info: logInfo(config),\n warn: logWarn(config),\n error: logError(config),\n }\n}\n","export const messages = {\n noActiveCalendarsGlobal:\n 'No active calendars found for the configured date range - returning empty routes and stops',\n noActiveCalendarsForRoute: (routeId: string) =>\n `route_id ${routeId} has no active calendars in range - skipping directions`,\n noActiveCalendarsForDirection: (\n routeId: string,\n directionId: string | number,\n ) =>\n `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,\n routeHasNoDirections: (routeId: string) =>\n `route_id ${routeId} has no directions - skipping`,\n stopNotFound: (\n routeId: string,\n directionId: string | number,\n stopId: string,\n ) =>\n `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`,\n}\n","import { join } from 'path'\nimport { I18n } from 'i18n'\n\nimport { Config } from '../../types/global_interfaces.ts'\nimport { getPathToViewsFolder } from '../file-utils.ts'\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\n}\n"],"mappings":"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","names":["dirname","join","fileURLToPath","openDb","untildify","config","config","config","join","config","join","config","stoptimes","join","openDb","untildify","dirname","fileURLToPath","port","resolve"]}
|
|
@@ -498,8 +498,8 @@ function generateTransitDeparturesWidgetJson(config) {
|
|
|
498
498
|
);
|
|
499
499
|
const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
|
|
500
500
|
return {
|
|
501
|
-
routes: removeNulls(sortedRoutes),
|
|
502
|
-
stops: removeNulls(sortedStops)
|
|
501
|
+
routes: arrayOfArrays(removeNulls(sortedRoutes)),
|
|
502
|
+
stops: arrayOfArrays(removeNulls(sortedStops))
|
|
503
503
|
};
|
|
504
504
|
}
|
|
505
505
|
function removeNulls(data) {
|
|
@@ -520,6 +520,15 @@ function removeNulls(data) {
|
|
|
520
520
|
return data;
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
|
+
function arrayOfArrays(array) {
|
|
524
|
+
if (array.length === 0) {
|
|
525
|
+
return { fields: [], rows: [] };
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
fields: Object.keys(array[0]),
|
|
529
|
+
rows: array.map((item) => Object.values(item))
|
|
530
|
+
};
|
|
531
|
+
}
|
|
523
532
|
function formatWhereClause(key, value) {
|
|
524
533
|
if (Array.isArray(value)) {
|
|
525
534
|
let whereClause = `${sqlString.escapeId(key)} IN (${value.filter((v) => v !== null).map((v) => sqlString.escape(v)).join(", ")})`;
|
|
@@ -588,11 +597,11 @@ async function transitDeparturesWidget(initialConfig) {
|
|
|
588
597
|
const { routes, stops } = generateTransitDeparturesWidgetJson(config);
|
|
589
598
|
await writeFile(
|
|
590
599
|
path.join(outputPath, "data", "routes.json"),
|
|
591
|
-
JSON.stringify(routes
|
|
600
|
+
JSON.stringify(routes)
|
|
592
601
|
);
|
|
593
602
|
await writeFile(
|
|
594
603
|
path.join(outputPath, "data", "stops.json"),
|
|
595
|
-
JSON.stringify(stops
|
|
604
|
+
JSON.stringify(stops)
|
|
596
605
|
);
|
|
597
606
|
const html = await generateTransitDeparturesWidgetHtml(config);
|
|
598
607
|
await writeFile(path.join(outputPath, "index.html"), html);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/logging/log.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/config/defaults.ts","../../src/lib/utils.ts","../../src/lib/logging/messages.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport PrettyError from 'pretty-error'\n\nimport { getConfig } from '../lib/file-utils.ts'\nimport { formatError } from '../lib/logging/log.ts'\nimport transitDeparturesWidget from '../index.ts'\n\nconst pe = new PrettyError()\n\nconst argv = yargs(hideBin(process.argv))\n .usage('Usage: $0 --config ./config.json')\n .help()\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .option('s', {\n alias: 'skipImport',\n describe: 'Don’t import GTFS file.',\n type: 'boolean',\n })\n .default('skipImport', undefined)\n .parseSync()\n\nconst handleError = (error: any) => {\n const text = error || 'Unknown Error'\n process.stdout.write(`\\n${formatError(text)}\\n`)\n console.error(pe.render(error))\n process.exit(1)\n}\n\nconst setupImport = async () => {\n const config = await getConfig(argv)\n await transitDeparturesWidget(config)\n process.exit()\n}\n\nsetupImport().catch(handleError)\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n access,\n cp,\n copyFile,\n mkdir,\n readdir,\n readFile,\n rm,\n} from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to this module's folder.\n */\nexport function getPathToThisModuleFolder() {\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to this module's folder\n let distFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n distFolderPath = resolve(__dirname, '../../')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n distFolderPath = resolve(__dirname, '../')\n } else {\n // In case it's neither, fallback to project root\n distFolderPath = resolve(__dirname, '../../')\n }\n\n return distFolderPath\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n return join(getPathToThisModuleFolder(), 'views/default')\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n const thisModuleFolderPath = getPathToThisModuleFolder()\n\n const foldersToCopy = ['css', 'js']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n\n // Copy js and css libraries from node_modules\n await copyFile(\n join(thisModuleFolderPath, 'dist/frontend_libraries/pbf.js'),\n join(outputPath, 'js/pbf.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/gtfs-realtime.browser.proto.js',\n ),\n join(outputPath, 'js/gtfs-realtime.browser.proto.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.js',\n ),\n join(outputPath, 'js/accessible-autocomplete.min.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.css',\n ),\n join(outputPath, 'css/accessible-autocomplete.min.css'),\n )\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../../types/global_interfaces.ts'\n\nexport type Logger = {\n info: (text: string, overwrite?: boolean) => void\n warn: (text: string) => void\n error: (text: string) => void\n}\n\nconst formatWarning = (text: string) => {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\nexport const formatError = (error: any) => {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n\nconst logInfo = (config: Config) => {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite?: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\nconst logWarn = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\nconst logError = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Create a structured logger with consistent methods.\n */\nexport function createLogger(config: Config): Logger {\n return {\n info: logInfo(config),\n warn: logWarn(config),\n error: logError(config),\n }\n}\n","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb, type ConfigAgency } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { createLogger } from './logging/log.ts'\nimport { setDefaultConfig } from './config/defaults.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n const logger = createLogger(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logger.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsPath = config.agency.gtfs_static_path\n const gtfsUrl = config.agency.gtfs_static_url\n\n if (!gtfsPath && !gtfsUrl) {\n throw new Error(\n 'Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json.',\n )\n }\n\n const agencyImportConfig: ConfigAgency = gtfsPath\n ? { path: gtfsPath }\n : { url: gtfsUrl as string }\n\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [agencyImportConfig],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n logger.info(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n logger.info(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n logger.info(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { join } from 'path'\nimport { I18n } from 'i18n'\n\nimport { Config } from '../../types/global_interfaces.ts'\nimport { getPathToViewsFolder } from '../file-utils.ts'\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\n}\n","import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport {\n ConfigWithI18n,\n GTFSCalendar,\n GTFSRoute,\n GTFSRouteDirection,\n GTFSStop,\n} from '../types/gtfs.ts'\nimport { createLogger } from './logging/log.ts'\nimport { messages } from './logging/messages.ts'\nexport { setDefaultConfig } from './config/defaults.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config): GTFSCalendar[] => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route: GTFSRoute) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: GTFSRoute, config: ConfigWithI18n) {\n const logger = createLogger(config)\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n .filter((direction) => direction.direction_id !== undefined)\n .map((direction) => ({\n direction_id: direction.direction_id as number | string,\n direction: direction.direction,\n }))\n\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsForRoute(route.route_id))\n return []\n }\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: GTFSCalendar) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: config.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n if (!longestTripStoptimes) {\n return []\n }\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\nfunction getStopsForDirection(\n route: GTFSRoute,\n direction: GTFSRouteDirection,\n config: Config,\n stopCache?: Map<string, GTFSStop>,\n) {\n const logger = createLogger(config)\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(\n messages.noActiveCalendarsForDirection(\n route.route_id,\n direction.direction_id,\n ),\n )\n return []\n }\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar: GTFSCalendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stopFields: (keyof GTFSStop)[] = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const missingStopIds = stopCache\n ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId))\n : deduplicatedStopIds\n\n const fetchedStops = missingStopIds.length\n ? (getStops as unknown as (query: any, fields?: any) => GTFSStop[])(\n { stop_id: missingStopIds },\n stopFields,\n )\n : []\n\n if (stopCache) {\n for (const stop of fetchedStops) {\n stopCache.set(stop.stop_id, stop)\n }\n }\n\n return deduplicatedStopIds\n .map((stopId: string) => {\n const stop =\n stopCache?.get(stopId) ??\n fetchedStops.find((candidate) => candidate.stop_id === stopId)\n\n if (!stop) {\n logger.warn(\n messages.stopNotFound(route.route_id, direction.direction_id, stopId),\n )\n }\n\n return stop\n })\n .filter(Boolean) as GTFSStop[]\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: ConfigWithI18n) {\n const templateVars = {\n config,\n __: config.__,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: ConfigWithI18n) {\n const logger = createLogger(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsGlobal)\n return { routes: [], stops: [] }\n }\n\n const routes = getRoutes() as GTFSRoute[]\n const stops: GTFSStop[] = []\n const filteredRoutes: GTFSRoute[] = []\n const stopCache = new Map<string, GTFSStop>()\n\n for (const route of routes) {\n const routeWithFullName: GTFSRoute = {\n ...route,\n route_full_name: formatRouteName(route),\n }\n\n const directions = getDirectionsForRoute(routeWithFullName, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logger.warn(messages.routeHasNoDirections(route.route_id))\n continue\n }\n\n const directionsWithData = directions\n .map((direction) => {\n const directionStops = getStopsForDirection(\n routeWithFullName,\n direction,\n config,\n stopCache,\n )\n\n if (directionStops.length === 0) {\n return null\n }\n\n stops.push(...directionStops)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: GTFSCalendar) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n\n return {\n ...direction,\n stopIds: directionStops.map((stop) => stop.stop_id),\n tripIds: trips.map((trip) => trip.trip_id),\n }\n })\n .filter(Boolean) as GTFSRouteDirection[]\n\n if (directionsWithData.length === 0) {\n continue\n }\n\n filteredRoutes.push({\n ...routeWithFullName,\n directions: directionsWithData,\n })\n }\n\n // Sort routes deterministically, handling mixed numeric/alphanumeric IDs\n const sortedRoutes = [...filteredRoutes].sort((a, b) => {\n const aShort = a.route_short_name ?? ''\n const bShort = b.route_short_name ?? ''\n const aNum = Number.parseInt(aShort, 10)\n const bNum = Number.parseInt(bShort, 10)\n\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {\n return aNum - bNum\n }\n\n if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {\n return 1\n }\n\n if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {\n return -1\n }\n\n return aShort.localeCompare(bShort, undefined, {\n numeric: true,\n sensitivity: 'base',\n })\n })\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => ({\n ...stop,\n is_parent_station: true,\n })),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (\n data !== null &&\n typeof data === 'object' &&\n Object.getPrototypeOf(data) === Object.prototype\n ) {\n return Object.entries(data).reduce<Record<string, unknown>>(\n (acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n },\n {},\n )\n } else {\n return data\n }\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","export const messages = {\n noActiveCalendarsGlobal:\n 'No active calendars found for the configured date range - returning empty routes and stops',\n noActiveCalendarsForRoute: (routeId: string) =>\n `route_id ${routeId} has no active calendars in range - skipping directions`,\n noActiveCalendarsForDirection: (\n routeId: string,\n directionId: string | number,\n ) =>\n `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,\n routeHasNoDirections: (routeId: string) =>\n `route_id ${routeId} has no directions - skipping`,\n stopNotFound: (\n routeId: string,\n directionId: string | number,\n stopId: string,\n ) =>\n `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`,\n}\n"],"mappings":"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","names":["argv","error","openDb","untildify","join","join","stoptimes","openDb","untildify"]}
|
|
1
|
+
{"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/logging/log.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/config/defaults.ts","../../src/lib/utils.ts","../../src/lib/logging/messages.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport PrettyError from 'pretty-error'\n\nimport { getConfig } from '../lib/file-utils.ts'\nimport { formatError } from '../lib/logging/log.ts'\nimport transitDeparturesWidget from '../index.ts'\n\nconst pe = new PrettyError()\n\nconst argv = yargs(hideBin(process.argv))\n .usage('Usage: $0 --config ./config.json')\n .help()\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .option('s', {\n alias: 'skipImport',\n describe: 'Don’t import GTFS file.',\n type: 'boolean',\n })\n .default('skipImport', undefined)\n .parseSync()\n\nconst handleError = (error: any) => {\n const text = error || 'Unknown Error'\n process.stdout.write(`\\n${formatError(text)}\\n`)\n console.error(pe.render(error))\n process.exit(1)\n}\n\nconst setupImport = async () => {\n const config = await getConfig(argv)\n await transitDeparturesWidget(config)\n process.exit()\n}\n\nsetupImport().catch(handleError)\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n access,\n cp,\n copyFile,\n mkdir,\n readdir,\n readFile,\n rm,\n} from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to this module's folder.\n */\nexport function getPathToThisModuleFolder() {\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to this module's folder\n let distFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n distFolderPath = resolve(__dirname, '../../')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n distFolderPath = resolve(__dirname, '../')\n } else {\n // In case it's neither, fallback to project root\n distFolderPath = resolve(__dirname, '../../')\n }\n\n return distFolderPath\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n return join(getPathToThisModuleFolder(), 'views/default')\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n const thisModuleFolderPath = getPathToThisModuleFolder()\n\n const foldersToCopy = ['css', 'js']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n\n // Copy js and css libraries from node_modules\n await copyFile(\n join(thisModuleFolderPath, 'dist/frontend_libraries/pbf.js'),\n join(outputPath, 'js/pbf.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/gtfs-realtime.browser.proto.js',\n ),\n join(outputPath, 'js/gtfs-realtime.browser.proto.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.js',\n ),\n join(outputPath, 'js/accessible-autocomplete.min.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.css',\n ),\n join(outputPath, 'css/accessible-autocomplete.min.css'),\n )\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../../types/global_interfaces.ts'\n\nexport type Logger = {\n info: (text: string, overwrite?: boolean) => void\n warn: (text: string) => void\n error: (text: string) => void\n}\n\nconst formatWarning = (text: string) => {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\nexport const formatError = (error: any) => {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n\nconst logInfo = (config: Config) => {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite?: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\nconst logWarn = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\nconst logError = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Create a structured logger with consistent methods.\n */\nexport function createLogger(config: Config): Logger {\n return {\n info: logInfo(config),\n warn: logWarn(config),\n error: logError(config),\n }\n}\n","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb, type ConfigAgency } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { createLogger } from './logging/log.ts'\nimport { setDefaultConfig } from './config/defaults.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n const logger = createLogger(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logger.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsPath = config.agency.gtfs_static_path\n const gtfsUrl = config.agency.gtfs_static_url\n\n if (!gtfsPath && !gtfsUrl) {\n throw new Error(\n 'Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json.',\n )\n }\n\n const agencyImportConfig: ConfigAgency = gtfsPath\n ? { path: gtfsPath }\n : { url: gtfsUrl as string }\n\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [agencyImportConfig],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n logger.info(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n logger.info(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n logger.info(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { join } from 'path'\nimport { I18n } from 'i18n'\n\nimport { Config } from '../../types/global_interfaces.ts'\nimport { getPathToViewsFolder } from '../file-utils.ts'\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\n}\n","import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport {\n ConfigWithI18n,\n GTFSCalendar,\n GTFSRoute,\n GTFSRouteDirection,\n GTFSStop,\n} from '../types/gtfs.ts'\nimport { createLogger } from './logging/log.ts'\nimport { messages } from './logging/messages.ts'\nexport { setDefaultConfig } from './config/defaults.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config): GTFSCalendar[] => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route: GTFSRoute) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: GTFSRoute, config: ConfigWithI18n) {\n const logger = createLogger(config)\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n .filter((direction) => direction.direction_id !== undefined)\n .map((direction) => ({\n direction_id: direction.direction_id as number | string,\n direction: direction.direction,\n }))\n\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsForRoute(route.route_id))\n return []\n }\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: GTFSCalendar) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: config.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n if (!longestTripStoptimes) {\n return []\n }\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\nfunction getStopsForDirection(\n route: GTFSRoute,\n direction: GTFSRouteDirection,\n config: Config,\n stopCache?: Map<string, GTFSStop>,\n) {\n const logger = createLogger(config)\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(\n messages.noActiveCalendarsForDirection(\n route.route_id,\n direction.direction_id,\n ),\n )\n return []\n }\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar: GTFSCalendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stopFields: (keyof GTFSStop)[] = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const missingStopIds = stopCache\n ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId))\n : deduplicatedStopIds\n\n const fetchedStops = missingStopIds.length\n ? (getStops as unknown as (query: any, fields?: any) => GTFSStop[])(\n { stop_id: missingStopIds },\n stopFields,\n )\n : []\n\n if (stopCache) {\n for (const stop of fetchedStops) {\n stopCache.set(stop.stop_id, stop)\n }\n }\n\n return deduplicatedStopIds\n .map((stopId: string) => {\n const stop =\n stopCache?.get(stopId) ??\n fetchedStops.find((candidate) => candidate.stop_id === stopId)\n\n if (!stop) {\n logger.warn(\n messages.stopNotFound(route.route_id, direction.direction_id, stopId),\n )\n }\n\n return stop\n })\n .filter(Boolean) as GTFSStop[]\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: ConfigWithI18n) {\n const templateVars = {\n config,\n __: config.__,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: ConfigWithI18n) {\n const logger = createLogger(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsGlobal)\n return { routes: [], stops: [] }\n }\n\n const routes = getRoutes() as GTFSRoute[]\n const stops: GTFSStop[] = []\n const filteredRoutes: GTFSRoute[] = []\n const stopCache = new Map<string, GTFSStop>()\n\n for (const route of routes) {\n const routeWithFullName: GTFSRoute = {\n ...route,\n route_full_name: formatRouteName(route),\n }\n\n const directions = getDirectionsForRoute(routeWithFullName, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logger.warn(messages.routeHasNoDirections(route.route_id))\n continue\n }\n\n const directionsWithData = directions\n .map((direction) => {\n const directionStops = getStopsForDirection(\n routeWithFullName,\n direction,\n config,\n stopCache,\n )\n\n if (directionStops.length === 0) {\n return null\n }\n\n stops.push(...directionStops)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: GTFSCalendar) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n\n return {\n ...direction,\n stopIds: directionStops.map((stop) => stop.stop_id),\n tripIds: trips.map((trip) => trip.trip_id),\n }\n })\n .filter(Boolean) as GTFSRouteDirection[]\n\n if (directionsWithData.length === 0) {\n continue\n }\n\n filteredRoutes.push({\n ...routeWithFullName,\n directions: directionsWithData,\n })\n }\n\n // Sort routes deterministically, handling mixed numeric/alphanumeric IDs\n const sortedRoutes = [...filteredRoutes].sort((a, b) => {\n const aShort = a.route_short_name ?? ''\n const bShort = b.route_short_name ?? ''\n const aNum = Number.parseInt(aShort, 10)\n const bNum = Number.parseInt(bShort, 10)\n\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {\n return aNum - bNum\n }\n\n if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {\n return 1\n }\n\n if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {\n return -1\n }\n\n return aShort.localeCompare(bShort, undefined, {\n numeric: true,\n sensitivity: 'base',\n })\n })\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => ({\n ...stop,\n is_parent_station: true,\n })),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: arrayOfArrays(removeNulls(sortedRoutes)),\n stops: arrayOfArrays(removeNulls(sortedStops)),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (\n data !== null &&\n typeof data === 'object' &&\n Object.getPrototypeOf(data) === Object.prototype\n ) {\n return Object.entries(data).reduce<Record<string, unknown>>(\n (acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n },\n {},\n )\n } else {\n return data\n }\n}\n\n/*\n * Convert an array of objects into an Array-of-arrays JSON: { \"fields\": [...], \"rows\": [[...], ...] }\n */\nfunction arrayOfArrays(array: any[]): { fields: string[]; rows: any[][] } {\n if (array.length === 0) {\n return { fields: [], rows: [] }\n }\n\n return {\n fields: Object.keys(array[0]),\n rows: array.map((item) => Object.values(item)),\n }\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","export const messages = {\n noActiveCalendarsGlobal:\n 'No active calendars found for the configured date range - returning empty routes and stops',\n noActiveCalendarsForRoute: (routeId: string) =>\n `route_id ${routeId} has no active calendars in range - skipping directions`,\n noActiveCalendarsForDirection: (\n routeId: string,\n directionId: string | number,\n ) =>\n `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,\n routeHasNoDirections: (routeId: string) =>\n `route_id ${routeId} has no directions - skipping`,\n stopNotFound: (\n routeId: string,\n directionId: string | number,\n stopId: string,\n ) =>\n `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`,\n}\n"],"mappings":"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","names":["argv","error","openDb","untildify","join","join","stoptimes","openDb","untildify"]}
|
package/dist/index.js
CHANGED
|
@@ -464,8 +464,8 @@ function generateTransitDeparturesWidgetJson(config) {
|
|
|
464
464
|
);
|
|
465
465
|
const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
|
|
466
466
|
return {
|
|
467
|
-
routes: removeNulls(sortedRoutes),
|
|
468
|
-
stops: removeNulls(sortedStops)
|
|
467
|
+
routes: arrayOfArrays(removeNulls(sortedRoutes)),
|
|
468
|
+
stops: arrayOfArrays(removeNulls(sortedStops))
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
471
|
function removeNulls(data) {
|
|
@@ -486,6 +486,15 @@ function removeNulls(data) {
|
|
|
486
486
|
return data;
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
|
+
function arrayOfArrays(array) {
|
|
490
|
+
if (array.length === 0) {
|
|
491
|
+
return { fields: [], rows: [] };
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
fields: Object.keys(array[0]),
|
|
495
|
+
rows: array.map((item) => Object.values(item))
|
|
496
|
+
};
|
|
497
|
+
}
|
|
489
498
|
function formatWhereClause(key, value) {
|
|
490
499
|
if (Array.isArray(value)) {
|
|
491
500
|
let whereClause = `${sqlString.escapeId(key)} IN (${value.filter((v) => v !== null).map((v) => sqlString.escape(v)).join(", ")})`;
|
|
@@ -554,11 +563,11 @@ async function transitDeparturesWidget(initialConfig) {
|
|
|
554
563
|
const { routes, stops } = generateTransitDeparturesWidgetJson(config);
|
|
555
564
|
await writeFile(
|
|
556
565
|
path.join(outputPath, "data", "routes.json"),
|
|
557
|
-
JSON.stringify(routes
|
|
566
|
+
JSON.stringify(routes)
|
|
558
567
|
);
|
|
559
568
|
await writeFile(
|
|
560
569
|
path.join(outputPath, "data", "stops.json"),
|
|
561
|
-
JSON.stringify(stops
|
|
570
|
+
JSON.stringify(stops)
|
|
562
571
|
);
|
|
563
572
|
const html = await generateTransitDeparturesWidgetHtml(config);
|
|
564
573
|
await writeFile(path.join(outputPath, "index.html"), html);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/logging/log.ts","../src/lib/config/defaults.ts","../src/lib/utils.ts","../src/lib/logging/messages.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb, type ConfigAgency } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { createLogger } from './logging/log.ts'\nimport { setDefaultConfig } from './config/defaults.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n const logger = createLogger(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logger.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsPath = config.agency.gtfs_static_path\n const gtfsUrl = config.agency.gtfs_static_url\n\n if (!gtfsPath && !gtfsUrl) {\n throw new Error(\n 'Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json.',\n )\n }\n\n const agencyImportConfig: ConfigAgency = gtfsPath\n ? { path: gtfsPath }\n : { url: gtfsUrl as string }\n\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [agencyImportConfig],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n logger.info(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n logger.info(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n logger.info(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n access,\n cp,\n copyFile,\n mkdir,\n readdir,\n readFile,\n rm,\n} from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to this module's folder.\n */\nexport function getPathToThisModuleFolder() {\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to this module's folder\n let distFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n distFolderPath = resolve(__dirname, '../../')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n distFolderPath = resolve(__dirname, '../')\n } else {\n // In case it's neither, fallback to project root\n distFolderPath = resolve(__dirname, '../../')\n }\n\n return distFolderPath\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n return join(getPathToThisModuleFolder(), 'views/default')\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n const thisModuleFolderPath = getPathToThisModuleFolder()\n\n const foldersToCopy = ['css', 'js']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n\n // Copy js and css libraries from node_modules\n await copyFile(\n join(thisModuleFolderPath, 'dist/frontend_libraries/pbf.js'),\n join(outputPath, 'js/pbf.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/gtfs-realtime.browser.proto.js',\n ),\n join(outputPath, 'js/gtfs-realtime.browser.proto.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.js',\n ),\n join(outputPath, 'js/accessible-autocomplete.min.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.css',\n ),\n join(outputPath, 'css/accessible-autocomplete.min.css'),\n )\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../../types/global_interfaces.ts'\n\nexport type Logger = {\n info: (text: string, overwrite?: boolean) => void\n warn: (text: string) => void\n error: (text: string) => void\n}\n\nconst formatWarning = (text: string) => {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\nexport const formatError = (error: any) => {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n\nconst logInfo = (config: Config) => {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite?: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\nconst logWarn = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\nconst logError = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Create a structured logger with consistent methods.\n */\nexport function createLogger(config: Config): Logger {\n return {\n info: logInfo(config),\n warn: logWarn(config),\n error: logError(config),\n }\n}\n","import { join } from 'path'\nimport { I18n } from 'i18n'\n\nimport { Config } from '../../types/global_interfaces.ts'\nimport { getPathToViewsFolder } from '../file-utils.ts'\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\n}\n","import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport {\n ConfigWithI18n,\n GTFSCalendar,\n GTFSRoute,\n GTFSRouteDirection,\n GTFSStop,\n} from '../types/gtfs.ts'\nimport { createLogger } from './logging/log.ts'\nimport { messages } from './logging/messages.ts'\nexport { setDefaultConfig } from './config/defaults.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config): GTFSCalendar[] => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route: GTFSRoute) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: GTFSRoute, config: ConfigWithI18n) {\n const logger = createLogger(config)\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n .filter((direction) => direction.direction_id !== undefined)\n .map((direction) => ({\n direction_id: direction.direction_id as number | string,\n direction: direction.direction,\n }))\n\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsForRoute(route.route_id))\n return []\n }\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: GTFSCalendar) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: config.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n if (!longestTripStoptimes) {\n return []\n }\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\nfunction getStopsForDirection(\n route: GTFSRoute,\n direction: GTFSRouteDirection,\n config: Config,\n stopCache?: Map<string, GTFSStop>,\n) {\n const logger = createLogger(config)\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(\n messages.noActiveCalendarsForDirection(\n route.route_id,\n direction.direction_id,\n ),\n )\n return []\n }\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar: GTFSCalendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stopFields: (keyof GTFSStop)[] = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const missingStopIds = stopCache\n ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId))\n : deduplicatedStopIds\n\n const fetchedStops = missingStopIds.length\n ? (getStops as unknown as (query: any, fields?: any) => GTFSStop[])(\n { stop_id: missingStopIds },\n stopFields,\n )\n : []\n\n if (stopCache) {\n for (const stop of fetchedStops) {\n stopCache.set(stop.stop_id, stop)\n }\n }\n\n return deduplicatedStopIds\n .map((stopId: string) => {\n const stop =\n stopCache?.get(stopId) ??\n fetchedStops.find((candidate) => candidate.stop_id === stopId)\n\n if (!stop) {\n logger.warn(\n messages.stopNotFound(route.route_id, direction.direction_id, stopId),\n )\n }\n\n return stop\n })\n .filter(Boolean) as GTFSStop[]\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: ConfigWithI18n) {\n const templateVars = {\n config,\n __: config.__,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: ConfigWithI18n) {\n const logger = createLogger(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsGlobal)\n return { routes: [], stops: [] }\n }\n\n const routes = getRoutes() as GTFSRoute[]\n const stops: GTFSStop[] = []\n const filteredRoutes: GTFSRoute[] = []\n const stopCache = new Map<string, GTFSStop>()\n\n for (const route of routes) {\n const routeWithFullName: GTFSRoute = {\n ...route,\n route_full_name: formatRouteName(route),\n }\n\n const directions = getDirectionsForRoute(routeWithFullName, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logger.warn(messages.routeHasNoDirections(route.route_id))\n continue\n }\n\n const directionsWithData = directions\n .map((direction) => {\n const directionStops = getStopsForDirection(\n routeWithFullName,\n direction,\n config,\n stopCache,\n )\n\n if (directionStops.length === 0) {\n return null\n }\n\n stops.push(...directionStops)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: GTFSCalendar) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n\n return {\n ...direction,\n stopIds: directionStops.map((stop) => stop.stop_id),\n tripIds: trips.map((trip) => trip.trip_id),\n }\n })\n .filter(Boolean) as GTFSRouteDirection[]\n\n if (directionsWithData.length === 0) {\n continue\n }\n\n filteredRoutes.push({\n ...routeWithFullName,\n directions: directionsWithData,\n })\n }\n\n // Sort routes deterministically, handling mixed numeric/alphanumeric IDs\n const sortedRoutes = [...filteredRoutes].sort((a, b) => {\n const aShort = a.route_short_name ?? ''\n const bShort = b.route_short_name ?? ''\n const aNum = Number.parseInt(aShort, 10)\n const bNum = Number.parseInt(bShort, 10)\n\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {\n return aNum - bNum\n }\n\n if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {\n return 1\n }\n\n if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {\n return -1\n }\n\n return aShort.localeCompare(bShort, undefined, {\n numeric: true,\n sensitivity: 'base',\n })\n })\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => ({\n ...stop,\n is_parent_station: true,\n })),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (\n data !== null &&\n typeof data === 'object' &&\n Object.getPrototypeOf(data) === Object.prototype\n ) {\n return Object.entries(data).reduce<Record<string, unknown>>(\n (acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n },\n {},\n )\n } else {\n return data\n }\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","export const messages = {\n noActiveCalendarsGlobal:\n 'No active calendars found for the configured date range - returning empty routes and stops',\n noActiveCalendarsForRoute: (routeId: string) =>\n `route_id ${routeId} has no active calendars in range - skipping directions`,\n noActiveCalendarsForDirection: (\n routeId: string,\n directionId: string | number,\n ) =>\n `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,\n routeHasNoDirections: (routeId: string) =>\n `route_id ${routeId} has no directions - skipping`,\n stopNotFound: (\n routeId: string,\n directionId: string | number,\n stopId: string,\n ) =>\n `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`,\n}\n"],"mappings":"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","names":["openDb","untildify","error","join","join","stoptimes","openDb","untildify"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/logging/log.ts","../src/lib/config/defaults.ts","../src/lib/utils.ts","../src/lib/logging/messages.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb, type ConfigAgency } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { createLogger } from './logging/log.ts'\nimport { setDefaultConfig } from './config/defaults.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n const logger = createLogger(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logger.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsPath = config.agency.gtfs_static_path\n const gtfsUrl = config.agency.gtfs_static_url\n\n if (!gtfsPath && !gtfsUrl) {\n throw new Error(\n 'Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json.',\n )\n }\n\n const agencyImportConfig: ConfigAgency = gtfsPath\n ? { path: gtfsPath }\n : { url: gtfsUrl as string }\n\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [agencyImportConfig],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n logger.info(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n logger.info(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n logger.info(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n access,\n cp,\n copyFile,\n mkdir,\n readdir,\n readFile,\n rm,\n} from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to this module's folder.\n */\nexport function getPathToThisModuleFolder() {\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to this module's folder\n let distFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n distFolderPath = resolve(__dirname, '../../')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n distFolderPath = resolve(__dirname, '../')\n } else {\n // In case it's neither, fallback to project root\n distFolderPath = resolve(__dirname, '../../')\n }\n\n return distFolderPath\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n return join(getPathToThisModuleFolder(), 'views/default')\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n const thisModuleFolderPath = getPathToThisModuleFolder()\n\n const foldersToCopy = ['css', 'js']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n\n // Copy js and css libraries from node_modules\n await copyFile(\n join(thisModuleFolderPath, 'dist/frontend_libraries/pbf.js'),\n join(outputPath, 'js/pbf.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/gtfs-realtime.browser.proto.js',\n ),\n join(outputPath, 'js/gtfs-realtime.browser.proto.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.js',\n ),\n join(outputPath, 'js/accessible-autocomplete.min.js'),\n )\n\n await copyFile(\n join(\n thisModuleFolderPath,\n 'dist/frontend_libraries/accessible-autocomplete.min.css',\n ),\n join(outputPath, 'css/accessible-autocomplete.min.css'),\n )\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../../types/global_interfaces.ts'\n\nexport type Logger = {\n info: (text: string, overwrite?: boolean) => void\n warn: (text: string) => void\n error: (text: string) => void\n}\n\nconst formatWarning = (text: string) => {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\nexport const formatError = (error: any) => {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n\nconst logInfo = (config: Config) => {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite?: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\nconst logWarn = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\nconst logError = (config: Config) => {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Create a structured logger with consistent methods.\n */\nexport function createLogger(config: Config): Logger {\n return {\n info: logInfo(config),\n warn: logWarn(config),\n error: logError(config),\n }\n}\n","import { join } from 'path'\nimport { I18n } from 'i18n'\n\nimport { Config } from '../../types/global_interfaces.ts'\nimport { getPathToViewsFolder } from '../file-utils.ts'\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n const config = Object.assign(defaults, initialConfig)\n const viewsFolderPath = getPathToViewsFolder(config)\n const i18n = new I18n({\n directory: join(viewsFolderPath, 'locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n const configWithI18n = Object.assign(config, {\n __: i18n.__,\n })\n return configWithI18n\n}\n","import { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport {\n ConfigWithI18n,\n GTFSCalendar,\n GTFSRoute,\n GTFSRouteDirection,\n GTFSStop,\n} from '../types/gtfs.ts'\nimport { createLogger } from './logging/log.ts'\nimport { messages } from './logging/messages.ts'\nexport { setDefaultConfig } from './config/defaults.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config): GTFSCalendar[] => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route: GTFSRoute) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: GTFSRoute, config: ConfigWithI18n) {\n const logger = createLogger(config)\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n .filter((direction) => direction.direction_id !== undefined)\n .map((direction) => ({\n direction_id: direction.direction_id as number | string,\n direction: direction.direction,\n }))\n\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsForRoute(route.route_id))\n return []\n }\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: GTFSCalendar) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: config.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n if (!longestTripStoptimes) {\n return []\n }\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\nfunction getStopsForDirection(\n route: GTFSRoute,\n direction: GTFSRouteDirection,\n config: Config,\n stopCache?: Map<string, GTFSStop>,\n) {\n const logger = createLogger(config)\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(\n messages.noActiveCalendarsForDirection(\n route.route_id,\n direction.direction_id,\n ),\n )\n return []\n }\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar: GTFSCalendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stopFields: (keyof GTFSStop)[] = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const missingStopIds = stopCache\n ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId))\n : deduplicatedStopIds\n\n const fetchedStops = missingStopIds.length\n ? (getStops as unknown as (query: any, fields?: any) => GTFSStop[])(\n { stop_id: missingStopIds },\n stopFields,\n )\n : []\n\n if (stopCache) {\n for (const stop of fetchedStops) {\n stopCache.set(stop.stop_id, stop)\n }\n }\n\n return deduplicatedStopIds\n .map((stopId: string) => {\n const stop =\n stopCache?.get(stopId) ??\n fetchedStops.find((candidate) => candidate.stop_id === stopId)\n\n if (!stop) {\n logger.warn(\n messages.stopNotFound(route.route_id, direction.direction_id, stopId),\n )\n }\n\n return stop\n })\n .filter(Boolean) as GTFSStop[]\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: ConfigWithI18n) {\n const templateVars = {\n config,\n __: config.__,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: ConfigWithI18n) {\n const logger = createLogger(config)\n const calendars = getCalendarsForDateRange(config)\n if (calendars.length === 0) {\n logger.warn(messages.noActiveCalendarsGlobal)\n return { routes: [], stops: [] }\n }\n\n const routes = getRoutes() as GTFSRoute[]\n const stops: GTFSStop[] = []\n const filteredRoutes: GTFSRoute[] = []\n const stopCache = new Map<string, GTFSStop>()\n\n for (const route of routes) {\n const routeWithFullName: GTFSRoute = {\n ...route,\n route_full_name: formatRouteName(route),\n }\n\n const directions = getDirectionsForRoute(routeWithFullName, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n logger.warn(messages.routeHasNoDirections(route.route_id))\n continue\n }\n\n const directionsWithData = directions\n .map((direction) => {\n const directionStops = getStopsForDirection(\n routeWithFullName,\n direction,\n config,\n stopCache,\n )\n\n if (directionStops.length === 0) {\n return null\n }\n\n stops.push(...directionStops)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: GTFSCalendar) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n\n return {\n ...direction,\n stopIds: directionStops.map((stop) => stop.stop_id),\n tripIds: trips.map((trip) => trip.trip_id),\n }\n })\n .filter(Boolean) as GTFSRouteDirection[]\n\n if (directionsWithData.length === 0) {\n continue\n }\n\n filteredRoutes.push({\n ...routeWithFullName,\n directions: directionsWithData,\n })\n }\n\n // Sort routes deterministically, handling mixed numeric/alphanumeric IDs\n const sortedRoutes = [...filteredRoutes].sort((a, b) => {\n const aShort = a.route_short_name ?? ''\n const bShort = b.route_short_name ?? ''\n const aNum = Number.parseInt(aShort, 10)\n const bNum = Number.parseInt(bShort, 10)\n\n if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {\n return aNum - bNum\n }\n\n if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {\n return 1\n }\n\n if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {\n return -1\n }\n\n return aShort.localeCompare(bShort, undefined, {\n numeric: true,\n sensitivity: 'base',\n })\n })\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => ({\n ...stop,\n is_parent_station: true,\n })),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: arrayOfArrays(removeNulls(sortedRoutes)),\n stops: arrayOfArrays(removeNulls(sortedStops)),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (\n data !== null &&\n typeof data === 'object' &&\n Object.getPrototypeOf(data) === Object.prototype\n ) {\n return Object.entries(data).reduce<Record<string, unknown>>(\n (acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n },\n {},\n )\n } else {\n return data\n }\n}\n\n/*\n * Convert an array of objects into an Array-of-arrays JSON: { \"fields\": [...], \"rows\": [[...], ...] }\n */\nfunction arrayOfArrays(array: any[]): { fields: string[]; rows: any[][] } {\n if (array.length === 0) {\n return { fields: [], rows: [] }\n }\n\n return {\n fields: Object.keys(array[0]),\n rows: array.map((item) => Object.values(item)),\n }\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","export const messages = {\n noActiveCalendarsGlobal:\n 'No active calendars found for the configured date range - returning empty routes and stops',\n noActiveCalendarsForRoute: (routeId: string) =>\n `route_id ${routeId} has no active calendars in range - skipping directions`,\n noActiveCalendarsForDirection: (\n routeId: string,\n directionId: string | number,\n ) =>\n `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,\n routeHasNoDirections: (routeId: string) =>\n `route_id ${routeId} has no directions - skipping`,\n stopNotFound: (\n routeId: string,\n directionId: string | number,\n stopId: string,\n ) =>\n `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`,\n}\n"],"mappings":"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","names":["openDb","untildify","error","join","join","stoptimes","openDb","untildify"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transit-departures-widget",
|
|
3
3
|
"description": "Build a realtime transit departures tool from GTFS and GTFS-Realtime.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.7.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"transit",
|
|
7
7
|
"gtfs",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"accessible-autocomplete": "^3.0.1",
|
|
42
42
|
"express": "^5.2.1",
|
|
43
|
-
"gtfs": "^4.18.
|
|
43
|
+
"gtfs": "^4.18.3",
|
|
44
44
|
"gtfs-realtime-pbf-js-module": "^1.0.0",
|
|
45
45
|
"i18n": "^0.15.3",
|
|
46
46
|
"js-beautify": "^1.15.4",
|
|
47
|
-
"lodash-es": "^4.17.
|
|
47
|
+
"lodash-es": "^4.17.23",
|
|
48
48
|
"pbf": "^4.0.1",
|
|
49
49
|
"pretty-error": "^4.0.0",
|
|
50
50
|
"pug": "^3.0.3",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@types/yargs": "^17.0.35",
|
|
70
70
|
"husky": "^9.1.7",
|
|
71
71
|
"lint-staged": "^16.2.7",
|
|
72
|
-
"prettier": "^3.8.
|
|
72
|
+
"prettier": "^3.8.1",
|
|
73
73
|
"tsup": "^8.5.1",
|
|
74
74
|
"typescript": "^5.9.3"
|
|
75
75
|
},
|