transit-departures-widget 2.4.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app/index.js CHANGED
@@ -1,15 +1,14 @@
1
1
  // src/app/index.ts
2
- import path from "node:path";
3
- import { fileURLToPath as fileURLToPath3 } from "node:url";
4
2
  import { readFileSync } from "node:fs";
3
+ import { join as join3 } from "node:path";
5
4
  import yargs from "yargs";
6
5
  import { openDb as openDb2 } from "gtfs";
7
- import express, { Router } from "express";
6
+ import untildify2 from "untildify";
7
+ import express from "express";
8
8
  import logger from "morgan";
9
9
 
10
10
  // src/lib/utils.ts
11
- import { fileURLToPath as fileURLToPath2 } from "url";
12
- import { dirname as dirname2, join as join2 } from "path";
11
+ import { join as join2 } from "path";
13
12
  import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
14
13
  import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
15
14
 
@@ -19,22 +18,27 @@ import { fileURLToPath } from "node:url";
19
18
  import beautify from "js-beautify";
20
19
  import pug from "pug";
21
20
  import untildify from "untildify";
22
- function getTemplatePath(templateFileName, config2) {
23
- let fullTemplateFileName = templateFileName;
24
- if (config2.noHead !== true) {
25
- fullTemplateFileName += "_full";
21
+ function getPathToViewsFolder(config2) {
22
+ if (config2.templatePath) {
23
+ return untildify(config2.templatePath);
26
24
  }
27
- if (config2.templatePath !== void 0) {
28
- return join(untildify(config2.templatePath), `${fullTemplateFileName}.pug`);
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ let viewsFolderPath;
27
+ if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
28
+ viewsFolderPath = resolve(__dirname, "../../views/widget");
29
+ } else if (__dirname.endsWith("/dist")) {
30
+ viewsFolderPath = resolve(__dirname, "../views/widget");
31
+ } else {
32
+ viewsFolderPath = resolve(__dirname, "views/widget");
29
33
  }
30
- return join(
31
- fileURLToPath(import.meta.url),
32
- "../../../views/widget",
33
- `${fullTemplateFileName}.pug`
34
- );
34
+ return viewsFolderPath;
35
+ }
36
+ function getPathToTemplateFile(templateFileName, config2) {
37
+ const fullTemplateFileName = config2.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
38
+ return join(getPathToViewsFolder(config2), fullTemplateFileName);
35
39
  }
36
40
  async function renderFile(templateFileName, templateVars, config2) {
37
- const templatePath = getTemplatePath(templateFileName, config2);
41
+ const templatePath = getPathToTemplateFile(templateFileName, config2);
38
42
  const html = await pug.renderFile(templatePath, templateVars);
39
43
  if (config2.beautify === true) {
40
44
  return beautify.html_beautify(html, { indent_size: 2 });
@@ -46,6 +50,26 @@ async function renderFile(templateFileName, templateVars, config2) {
46
50
  import sqlString from "sqlstring-sqlite";
47
51
  import toposort from "toposort";
48
52
  import i18n from "i18n";
53
+
54
+ // src/lib/log-utils.ts
55
+ import { noop } from "lodash-es";
56
+ import * as colors from "yoctocolors";
57
+ function logWarning(config2) {
58
+ if (config2.logFunction) {
59
+ return config2.logFunction;
60
+ }
61
+ return (text) => {
62
+ process.stdout.write(`
63
+ ${formatWarning(text)}
64
+ `);
65
+ };
66
+ }
67
+ function formatWarning(text) {
68
+ const warningMessage = `${colors.underline("Warning")}: ${text}`;
69
+ return colors.yellow(warningMessage);
70
+ }
71
+
72
+ // src/lib/utils.ts
49
73
  var getCalendarsForDateRange = (config2) => {
50
74
  const db = openDb(config2);
51
75
  let whereClause = "";
@@ -155,8 +179,9 @@ function getStopsForDirection(route, direction, config2) {
155
179
  );
156
180
  }
157
181
  function generateTransitDeparturesWidgetHtml(config2) {
182
+ const viewsFolderPath = getPathToViewsFolder(config2);
158
183
  i18n.configure({
159
- directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
184
+ directory: join2(viewsFolderPath, "locales"),
160
185
  defaultLocale: config2.locale,
161
186
  updateFiles: false
162
187
  });
@@ -279,9 +304,8 @@ var argv = yargs(process.argv).option("c", {
279
304
  type: "string"
280
305
  }).parseSync();
281
306
  var app = express();
282
- var router = Router();
283
- var configPath = argv.configPath || new URL("../../config.json", import.meta.url);
284
- var selectedConfig = JSON.parse(readFileSync(configPath).toString());
307
+ var configPath = argv.configPath || join3(process.cwd(), "config.json");
308
+ var selectedConfig = JSON.parse(readFileSync(configPath, "utf8"));
285
309
  var config = setDefaultConfig(selectedConfig);
286
310
  config.noHead = false;
287
311
  config.assetPath = "/";
@@ -294,7 +318,7 @@ try {
294
318
  );
295
319
  throw error;
296
320
  }
297
- router.get("/", async (request, response, next) => {
321
+ app.get("/", async (request, response, next) => {
298
322
  try {
299
323
  const html = await generateTransitDeparturesWidgetHtml(config);
300
324
  response.send(html);
@@ -302,7 +326,7 @@ router.get("/", async (request, response, next) => {
302
326
  next(error);
303
327
  }
304
328
  });
305
- router.get("/data/routes.json", async (request, response, next) => {
329
+ app.get("/data/routes.json", async (request, response, next) => {
306
330
  try {
307
331
  const { routes } = await generateTransitDeparturesWidgetJson(config);
308
332
  response.json(routes);
@@ -310,7 +334,7 @@ router.get("/data/routes.json", async (request, response, next) => {
310
334
  next(error);
311
335
  }
312
336
  });
313
- router.get("/data/stops.json", async (request, response, next) => {
337
+ app.get("/data/stops.json", async (request, response, next) => {
314
338
  try {
315
339
  const { stops } = await generateTransitDeparturesWidgetJson(config);
316
340
  response.json(stops);
@@ -318,15 +342,41 @@ router.get("/data/stops.json", async (request, response, next) => {
318
342
  next(error);
319
343
  }
320
344
  });
321
- app.set("views", path.join(fileURLToPath3(import.meta.url), "../../../views"));
345
+ app.set("views", getPathToViewsFolder(config));
322
346
  app.set("view engine", "pug");
323
347
  app.use(logger("dev"));
348
+ var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify2(config.templatePath);
349
+ app.use(express.static(staticAssetPath));
350
+ app.use((req, res) => {
351
+ res.status(404).send("Not Found");
352
+ });
324
353
  app.use(
325
- express.static(path.join(fileURLToPath3(import.meta.url), "../../../public"))
354
+ (err, req, res, next) => {
355
+ console.error(err.stack);
356
+ res.status(500).send("Something broke!");
357
+ }
326
358
  );
327
- app.use("/", router);
328
- app.set("port", process.env.PORT || 3e3);
329
- var server = app.listen(app.get("port"), () => {
330
- console.log(`Express server listening on port ${app.get("port")}`);
331
- });
359
+ var startServer = async (port2) => {
360
+ try {
361
+ await new Promise((resolve2, reject) => {
362
+ const server = app.listen(port2).once("listening", () => {
363
+ console.log(`Express server listening on port ${port2}`);
364
+ resolve2();
365
+ }).once("error", (err) => {
366
+ if (err.code === "EADDRINUSE") {
367
+ console.log(`Port ${port2} is in use, trying ${port2 + 1}`);
368
+ server.close();
369
+ resolve2(startServer(port2 + 1));
370
+ } else {
371
+ reject(err);
372
+ }
373
+ });
374
+ });
375
+ } catch (err) {
376
+ console.error("Failed to start server:", err);
377
+ process.exit(1);
378
+ }
379
+ };
380
+ var port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3e3;
381
+ startServer(port);
332
382
  //# sourceMappingURL=index.js.map
@@ -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.logFunction = console.log\n\ntry {\n openDb(config)\n} catch (error: any) {\n console.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists and import GTFS before running this app.`,\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 { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\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: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\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: Config) {\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: Config) {\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: Config) {\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 logWarning(config)(\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: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n 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 { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\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: string, config: Config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`)\n }\n\n return join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = 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":["fileURLToPath","openDb","fileURLToPath","dirname","join","config","config","config","stoptimes","join","dirname","fileURLToPath","openDb","fileURLToPath"]}
1
+ {"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\nimport untildify from 'untildify'\nimport express from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\nimport { getPathToViewsFolder } from '../lib/file-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()\n\nconst configPath =\n (argv.configPath as string) || join(process.cwd(), 'config.json')\nconst selectedConfig = JSON.parse(readFileSync(configPath, 'utf8'))\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.logFunction = console.log\n\ntry {\n openDb(config)\n} catch (error: any) {\n console.error(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists and import GTFS before running this app.`,\n )\n throw error\n}\n\n/*\n * Show the transit departures widget\n */\napp.get('/', async (request, response, next) => {\n try {\n const html = await generateTransitDeparturesWidgetHtml(config)\n response.send(html)\n } catch (error) {\n next(error)\n }\n})\n\n/*\n * Provide data\n */\napp.get('/data/routes.json', async (request, response, next) => {\n try {\n const { routes } = await generateTransitDeparturesWidgetJson(config)\n response.json(routes)\n } catch (error) {\n next(error)\n }\n})\n\napp.get('/data/stops.json', async (request, response, next) => {\n try {\n const { stops } = await generateTransitDeparturesWidgetJson(config)\n response.json(stops)\n } catch (error) {\n next(error)\n }\n})\n\napp.set('views', getPathToViewsFolder(config))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\n\n// Serve static assets\nconst staticAssetPath =\n config.templatePath === undefined\n ? getPathToViewsFolder(config)\n : untildify(config.templatePath)\n\napp.use(express.static(staticAssetPath))\n\n// Fallback 404 route\napp.use((req, res) => {\n res.status(404).send('Not Found')\n})\n\n// Error handling middleware\napp.use(\n (\n err: Error,\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n console.error(err.stack)\n res.status(500).send('Something broke!')\n },\n)\n\nconst startServer = async (port: number): Promise<void> => {\n try {\n await new Promise<void>((resolve, reject) => {\n const server = app\n .listen(port)\n .once('listening', () => {\n console.log(`Express server listening on port ${port}`)\n resolve()\n })\n .once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n console.log(`Port ${port} is in use, trying ${port + 1}`)\n server.close()\n resolve(startServer(port + 1))\n } else {\n reject(err)\n }\n })\n })\n } catch (err) {\n console.error('Failed to start server:', err)\n process.exit(1)\n }\n}\n\nconst port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000\nstartServer(port)\n","import { join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { getPathToViewsFolder, renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport { logWarning } from './log-utils.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\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: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\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: Config) {\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: Config) {\n const viewsFolderPath = getPathToViewsFolder(config)\n i18n.configure({\n directory: join(viewsFolderPath, '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: Config) {\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 logWarning(config)(\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: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n 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 { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * 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"],"mappings":"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","names":["join","openDb","untildify","join","config","config","config","config","stoptimes","join","join","openDb","untildify","port","resolve"]}
@@ -54,19 +54,9 @@ function getPathToViewsFolder(config) {
54
54
  }
55
55
  return viewsFolderPath;
56
56
  }
57
- function getTemplatePath(templateFileName, config) {
58
- let fullTemplateFileName = templateFileName;
59
- if (config.noHead !== true) {
60
- fullTemplateFileName += "_full";
61
- }
62
- if (config.templatePath !== void 0) {
63
- return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
64
- }
65
- return join(
66
- fileURLToPath(import.meta.url),
67
- "../../../views/widget",
68
- `${fullTemplateFileName}.pug`
69
- );
57
+ function getPathToTemplateFile(templateFileName, config) {
58
+ const fullTemplateFileName = config.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
59
+ return join(getPathToViewsFolder(config), fullTemplateFileName);
70
60
  }
71
61
  async function prepDirectory(outputPath, config) {
72
62
  try {
@@ -106,7 +96,7 @@ async function copyStaticAssets(config, outputPath) {
106
96
  }
107
97
  }
108
98
  async function renderFile(templateFileName, templateVars, config) {
109
- const templatePath = getTemplatePath(templateFileName, config);
99
+ const templatePath = getPathToTemplateFile(templateFileName, config);
110
100
  const html = await pug.renderFile(templatePath, templateVars);
111
101
  if (config.beautify === true) {
112
102
  return beautify.html_beautify(html, { indent_size: 2 });
@@ -135,6 +125,16 @@ function log(config) {
135
125
  process.stdout.write(text);
136
126
  };
137
127
  }
128
+ function logWarning(config) {
129
+ if (config.logFunction) {
130
+ return config.logFunction;
131
+ }
132
+ return (text) => {
133
+ process.stdout.write(`
134
+ ${formatWarning(text)}
135
+ `);
136
+ };
137
+ }
138
138
  function logError(config) {
139
139
  if (config.logFunction) {
140
140
  return config.logFunction;
@@ -145,6 +145,10 @@ ${formatError(text)}
145
145
  `);
146
146
  };
147
147
  }
148
+ function formatWarning(text) {
149
+ const warningMessage = `${colors.underline("Warning")}: ${text}`;
150
+ return colors.yellow(warningMessage);
151
+ }
148
152
  function formatError(error) {
149
153
  const messageText = error instanceof Error ? error.message : error;
150
154
  const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
@@ -164,8 +168,7 @@ import Timer from "timer-machine";
164
168
  import untildify2 from "untildify";
165
169
 
166
170
  // src/lib/utils.ts
167
- import { fileURLToPath as fileURLToPath2 } from "url";
168
- import { dirname as dirname2, join as join2 } from "path";
171
+ import { join as join2 } from "path";
169
172
  import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
170
173
  import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
171
174
  import sqlString from "sqlstring-sqlite";
@@ -280,8 +283,9 @@ function getStopsForDirection(route, direction, config) {
280
283
  );
281
284
  }
282
285
  function generateTransitDeparturesWidgetHtml(config) {
286
+ const viewsFolderPath = getPathToViewsFolder(config);
283
287
  i18n.configure({
284
- directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
288
+ directory: join2(viewsFolderPath, "locales"),
285
289
  defaultLocale: config.locale,
286
290
  updateFiles: false
287
291
  });
@@ -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 { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\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: string, config: Config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`)\n }\n\n return join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = 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 { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * 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'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logError(config)(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const 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(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n log(config)(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n log(config)(`${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 { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\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: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\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: Config) {\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: Config) {\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: Config) {\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 logWarning(config)(\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: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n 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","error","openDb","untildify","fileURLToPath","dirname","join","stoptimes","join","dirname","fileURLToPath","openDb","untildify"]}
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 { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * 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'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logError(config)(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const 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(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n log(config)(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { getPathToViewsFolder, renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport { logWarning } from './log-utils.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\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: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\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: Config) {\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: Config) {\n const viewsFolderPath = getPathToViewsFolder(config)\n i18n.configure({\n directory: join(viewsFolderPath, '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: Config) {\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 logWarning(config)(\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: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n 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","error","openDb","untildify","join","stoptimes","join","openDb","untildify"]}
package/dist/index.js CHANGED
@@ -29,19 +29,9 @@ function getPathToViewsFolder(config) {
29
29
  }
30
30
  return viewsFolderPath;
31
31
  }
32
- function getTemplatePath(templateFileName, config) {
33
- let fullTemplateFileName = templateFileName;
34
- if (config.noHead !== true) {
35
- fullTemplateFileName += "_full";
36
- }
37
- if (config.templatePath !== void 0) {
38
- return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
39
- }
40
- return join(
41
- fileURLToPath(import.meta.url),
42
- "../../../views/widget",
43
- `${fullTemplateFileName}.pug`
44
- );
32
+ function getPathToTemplateFile(templateFileName, config) {
33
+ const fullTemplateFileName = config.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
34
+ return join(getPathToViewsFolder(config), fullTemplateFileName);
45
35
  }
46
36
  async function prepDirectory(outputPath, config) {
47
37
  try {
@@ -81,7 +71,7 @@ async function copyStaticAssets(config, outputPath) {
81
71
  }
82
72
  }
83
73
  async function renderFile(templateFileName, templateVars, config) {
84
- const templatePath = getTemplatePath(templateFileName, config);
74
+ const templatePath = getPathToTemplateFile(templateFileName, config);
85
75
  const html = await pug.renderFile(templatePath, templateVars);
86
76
  if (config.beautify === true) {
87
77
  return beautify.html_beautify(html, { indent_size: 2 });
@@ -110,6 +100,16 @@ function log(config) {
110
100
  process.stdout.write(text);
111
101
  };
112
102
  }
103
+ function logWarning(config) {
104
+ if (config.logFunction) {
105
+ return config.logFunction;
106
+ }
107
+ return (text) => {
108
+ process.stdout.write(`
109
+ ${formatWarning(text)}
110
+ `);
111
+ };
112
+ }
113
113
  function logError(config) {
114
114
  if (config.logFunction) {
115
115
  return config.logFunction;
@@ -120,6 +120,10 @@ ${formatError(text)}
120
120
  `);
121
121
  };
122
122
  }
123
+ function formatWarning(text) {
124
+ const warningMessage = `${colors.underline("Warning")}: ${text}`;
125
+ return colors.yellow(warningMessage);
126
+ }
123
127
  function formatError(error) {
124
128
  const messageText = error instanceof Error ? error.message : error;
125
129
  const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
@@ -130,8 +134,7 @@ function formatError(error) {
130
134
  }
131
135
 
132
136
  // src/lib/utils.ts
133
- import { fileURLToPath as fileURLToPath2 } from "url";
134
- import { dirname as dirname2, join as join2 } from "path";
137
+ import { join as join2 } from "path";
135
138
  import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
136
139
  import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
137
140
  import sqlString from "sqlstring-sqlite";
@@ -246,8 +249,9 @@ function getStopsForDirection(route, direction, config) {
246
249
  );
247
250
  }
248
251
  function generateTransitDeparturesWidgetHtml(config) {
252
+ const viewsFolderPath = getPathToViewsFolder(config);
249
253
  i18n.configure({
250
- directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
254
+ directory: join2(viewsFolderPath, "locales"),
251
255
  defaultLocale: config.locale,
252
256
  updateFiles: false
253
257
  });
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'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logError(config)(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const 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(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n log(config)(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\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: string, config: Config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`)\n }\n\n return join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = 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 { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * 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 { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\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: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\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: Config) {\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: Config) {\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: Config) {\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 logWarning(config)(\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: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n 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":["openDb","untildify","error","fileURLToPath","dirname","join","stoptimes","join","dirname","fileURLToPath","openDb","untildify"]}
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'\nimport untildify from 'untildify'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: Config) {\n const config = setDefaultConfig(initialConfig)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n logError(config)(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key ?? 'unknown'\n\n const outputPath = config.outputPath\n ? untildify(config.outputPath)\n : path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const 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(outputPath, config)\n\n if (config.noHead !== true) {\n await copyStaticAssets(config, outputPath)\n }\n\n log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(outputPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(outputPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(outputPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n log(config)(\n `${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { access, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path to the views folder.\n */\nexport function getPathToViewsFolder(config: Config) {\n if (config.templatePath) {\n return untildify(config.templatePath)\n }\n\n const __dirname = dirname(fileURLToPath(import.meta.url))\n\n // Dynamically calculate the path to the views directory\n let viewsFolderPath\n if (__dirname.endsWith('/dist/bin') || __dirname.endsWith('/dist/app')) {\n // When the file is in 'dist/bin' or 'dist/app'\n viewsFolderPath = resolve(__dirname, '../../views/widget')\n } else if (__dirname.endsWith('/dist')) {\n // When the file is in 'dist'\n viewsFolderPath = resolve(__dirname, '../views/widget')\n } else {\n // In case it's neither, fallback to project root\n viewsFolderPath = resolve(__dirname, 'views/widget')\n }\n\n return viewsFolderPath\n}\n\n/*\n * Get the full path of a template file.\n */\nfunction getPathToTemplateFile(templateFileName: string, config: Config) {\n const fullTemplateFileName =\n config.noHead !== true\n ? `${templateFileName}_full.pug`\n : `${templateFileName}.pug`\n\n return join(getPathToViewsFolder(config), fullTemplateFileName)\n}\n\n/*\n * Prepare the outputPath directory for writing timetable files.\n */\nexport async function prepDirectory(outputPath: string, config: Config) {\n // Check if outputPath exists\n try {\n await access(outputPath)\n } catch (error: any) {\n try {\n await mkdir(outputPath, { recursive: true })\n await mkdir(join(outputPath, 'data'))\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${outputPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n }\n\n // Check if outputPath is empty\n const files = await readdir(outputPath)\n if (config.overwriteExistingFiles === false && files.length > 0) {\n throw new Error(\n `Output directory ${outputPath} is not empty. Please specify an empty directory.`,\n )\n }\n\n // Delete all files in outputPath if `overwriteExistingFiles` is true\n if (config.overwriteExistingFiles === true) {\n await rm(join(outputPath, '*'), { recursive: true, force: true })\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport async function copyStaticAssets(config: Config, outputPath: string) {\n const viewsFolderPath = getPathToViewsFolder(config)\n\n const foldersToCopy = ['css', 'js', 'img']\n\n for (const folder of foldersToCopy) {\n if (\n await access(join(viewsFolderPath, folder))\n .then(() => true)\n .catch(() => false)\n ) {\n await cp(join(viewsFolderPath, folder), join(outputPath, folder), {\n recursive: true,\n })\n }\n }\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: Config,\n) {\n const templatePath = getPathToTemplateFile(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import { clearLine, cursorTo } from 'node:readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { Config } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: Config) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true && process.stdout.isTTY) {\n clearLine(process.stdout, 0)\n cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: Config) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * 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 { join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { getPathToViewsFolder, renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { Config, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\nimport { logWarning } from './log-utils.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: Config) => {\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: Config) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\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: Config) {\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: Config) {\n const viewsFolderPath = getPathToViewsFolder(config)\n i18n.configure({\n directory: join(viewsFolderPath, '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: Config) {\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 logWarning(config)(\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: Config) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n overwriteExistingFiles: true,\n verbose: true,\n }\n\n 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":["openDb","untildify","error","join","stoptimes","join","openDb","untildify"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "transit-departures-widget",
3
3
  "description": "Build a realtime transit departures tool from GTFS and GTFS-Realtime.",
4
- "version": "2.4.4",
4
+ "version": "2.5.0",
5
5
  "keywords": [
6
6
  "transit",
7
7
  "gtfs",