transit-departures-widget 2.4.2 → 2.4.3

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
@@ -149,12 +149,7 @@ function getStopsForDirection(route, direction, config2) {
149
149
  []
150
150
  );
151
151
  deduplicatedStopIds.pop();
152
- const stopFields = [
153
- "stop_id",
154
- "stop_name",
155
- "stop_code",
156
- "parent_station"
157
- ];
152
+ const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
158
153
  if (config2.includeCoordinates) {
159
154
  stopFields.push("stop_lat", "stop_lon");
160
155
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\n\nimport express, { Router } from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\n\nconst argv = yargs(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()\nconst router = Router()\n\nconst configPath = (argv.configPath ||\n new URL('../../config.json', import.meta.url)) as string\n\nconst selectedConfig = JSON.parse(readFileSync(configPath).toString())\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.log = console.log\nconfig.logWarning = console.warn\nconfig.logError = console.error\n\ntry {\n openDb(config)\n} catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\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/*\n * Show the transit departures widget\n */\nrouter.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 */\nrouter.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\nrouter.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\napp.set('views', path.join(fileURLToPath(import.meta.url), '../../../views'))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\napp.use(\n express.static(path.join(fileURLToPath(import.meta.url), '../../../public')),\n)\n\napp.use('/', router)\napp.set('port', process.env.PORT || 3000)\n\nconst server = app.listen(app.get('port'), () => {\n console.log(`Express server listening on port ${app.get('port')}`)\n})\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { 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'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\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) {\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: Record<string, string>, config: IConfig) {\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\n const calendars = getCalendarsForDateRange(config)\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: Record<string, string>) => `'${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: i18n.__('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 return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => 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\n const stopFields = [\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 stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\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: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\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: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\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.is_parent_station = true\n return stop\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 (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\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 path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } 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 path.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 of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(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"],"mappings":"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","names":["path","fileURLToPath","openDb","fileURLToPath","config","config","config","stoptimes","fileURLToPath","openDb","path","fileURLToPath"]}
1
+ {"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\n\nimport express, { Router } from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\n\nconst argv = yargs(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()\nconst router = Router()\n\nconst configPath = (argv.configPath ||\n new URL('../../config.json', import.meta.url)) as string\n\nconst selectedConfig = JSON.parse(readFileSync(configPath).toString())\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.log = console.log\nconfig.logWarning = console.warn\nconfig.logError = console.error\n\ntry {\n openDb(config)\n} catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\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/*\n * Show the transit departures widget\n */\nrouter.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 */\nrouter.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\nrouter.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\napp.set('views', path.join(fileURLToPath(import.meta.url), '../../../views'))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\napp.use(\n express.static(path.join(fileURLToPath(import.meta.url), '../../../public')),\n)\n\napp.use('/', router)\napp.set('port', process.env.PORT || 3000)\n\nconst server = app.listen(app.get('port'), () => {\n console.log(`Express server listening on port ${app.get('port')}`)\n})\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { 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'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\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) {\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: Record<string, string>, config: IConfig) {\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\n const calendars = getCalendarsForDateRange(config)\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: Record<string, string>) => `'${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: i18n.__('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 return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => 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\n const stopFields = ['stop_id', 'stop_name', 'stop_code', 'parent_station']\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\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: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\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: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\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.is_parent_station = true\n return stop\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 (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\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 path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } 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 path.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 of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(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"],"mappings":"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","names":["path","fileURLToPath","openDb","fileURLToPath","config","config","config","stoptimes","fileURLToPath","openDb","path","fileURLToPath"]}
@@ -257,12 +257,7 @@ function getStopsForDirection(route, direction, config) {
257
257
  []
258
258
  );
259
259
  deduplicatedStopIds.pop();
260
- const stopFields = [
261
- "stop_id",
262
- "stop_name",
263
- "stop_code",
264
- "parent_station"
265
- ];
260
+ const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
266
261
  if (config.includeCoordinates) {
267
262
  stopFields.push("stop_lat", "stop_lon");
268
263
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/utils.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/log-utils.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 path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } 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 path.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 of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(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 readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\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) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\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\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\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 * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function 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","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\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\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${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(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { 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'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\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) {\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: Record<string, string>, config: IConfig) {\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\n const calendars = getCalendarsForDateRange(config)\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: Record<string, string>) => `'${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: i18n.__('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 return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => 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\n const stopFields = [\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 stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\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: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\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: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\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.is_parent_station = true\n return stop\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 (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\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"],"mappings":"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","names":["argv","path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
1
+ {"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/utils.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/log-utils.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 path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } 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 path.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 of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(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 readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\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) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\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\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\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 * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function 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","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\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\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${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(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { 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'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\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) {\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: Record<string, string>, config: IConfig) {\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\n const calendars = getCalendarsForDateRange(config)\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: Record<string, string>) => `'${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: i18n.__('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 return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => 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\n const stopFields = ['stop_id', 'stop_name', 'stop_code', 'parent_station']\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\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: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\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: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\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.is_parent_station = true\n return stop\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 (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\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"],"mappings":"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","names":["argv","path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
package/dist/index.js CHANGED
@@ -223,12 +223,7 @@ function getStopsForDirection(route, direction, config) {
223
223
  []
224
224
  );
225
225
  deduplicatedStopIds.pop();
226
- const stopFields = [
227
- "stop_id",
228
- "stop_name",
229
- "stop_code",
230
- "parent_station"
231
- ];
226
+ const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
232
227
  if (config.includeCoordinates) {
233
228
  stopFields.push("stop_lat", "stop_lon");
234
229
  }
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/log-utils.ts","../src/lib/utils.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\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\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${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(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } 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 path.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 of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(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 readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\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) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\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\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\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 * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function 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","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { 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'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\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) {\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: Record<string, string>, config: IConfig) {\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\n const calendars = getCalendarsForDateRange(config)\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: Record<string, string>) => `'${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: i18n.__('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 return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => 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\n const stopFields = [\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 stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\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: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\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: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\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.is_parent_station = true\n return stop\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 (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\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"],"mappings":"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","names":["path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
1
+ {"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/log-utils.ts","../src/lib/utils.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\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\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${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(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } 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 path.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 of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(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 readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\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) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\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\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\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 * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function 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","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { 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'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\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) {\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: Record<string, string>, config: IConfig) {\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\n const calendars = getCalendarsForDateRange(config)\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: Record<string, string>) => `'${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: i18n.__('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 return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => 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\n const stopFields = ['stop_id', 'stop_name', 'stop_code', 'parent_station']\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\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: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\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: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\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.is_parent_station = true\n return stop\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 (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\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"],"mappings":"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","names":["path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
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.2",
4
+ "version": "2.4.3",
5
5
  "keywords": [
6
6
  "transit",
7
7
  "gtfs",
@@ -37,8 +37,8 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "copy-dir": "^1.3.0",
40
- "express": "^4.19.2",
41
- "gtfs": "^4.12.0",
40
+ "express": "^4.21.0",
41
+ "gtfs": "^4.14.5",
42
42
  "i18n": "^0.15.1",
43
43
  "js-beautify": "^1.15.1",
44
44
  "lodash-es": "^4.17.21",
@@ -59,16 +59,16 @@
59
59
  "@types/js-beautify": "^1.14.3",
60
60
  "@types/lodash-es": "^4.17.12",
61
61
  "@types/morgan": "^1.9.9",
62
- "@types/node": "^20.14.9",
62
+ "@types/node": "^20.16.10",
63
63
  "@types/pug": "^2.0.10",
64
64
  "@types/timer-machine": "^1.1.3",
65
65
  "@types/toposort": "^2.0.7",
66
- "@types/yargs": "^17.0.32",
67
- "husky": "^9.0.11",
68
- "lint-staged": "^15.2.7",
69
- "prettier": "^3.3.2",
70
- "tsup": "^8.1.0",
71
- "typescript": "^5.5.3"
66
+ "@types/yargs": "^17.0.33",
67
+ "husky": "^9.1.6",
68
+ "lint-staged": "^15.2.10",
69
+ "prettier": "^3.3.3",
70
+ "tsup": "^8.3.0",
71
+ "typescript": "^5.6.2"
72
72
  },
73
73
  "engines": {
74
74
  "node": ">= 14.15.4"
@@ -81,6 +81,9 @@
81
81
  "@release-it/keep-a-changelog": {
82
82
  "filename": "CHANGELOG.md"
83
83
  }
84
+ },
85
+ "hooks": {
86
+ "after:bump": "npm run build"
84
87
  }
85
88
  },
86
89
  "prettier": {