transit-departures-widget 2.4.2 → 2.4.4
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 +17 -28
- package/dist/app/index.js.map +1 -1
- package/dist/bin/transit-departures-widget.js +76 -70
- package/dist/bin/transit-departures-widget.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +75 -69
- package/dist/index.js.map +1 -1
- package/package.json +13 -11
package/dist/app/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/app/index.ts
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import yargs from "yargs";
|
|
@@ -9,14 +9,13 @@ import logger from "morgan";
|
|
|
9
9
|
|
|
10
10
|
// src/lib/utils.ts
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
|
-
import { dirname, join } from "path";
|
|
12
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
13
13
|
import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
14
14
|
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
15
15
|
|
|
16
16
|
// src/lib/file-utils.ts
|
|
17
|
-
import
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import copydir from "copy-dir";
|
|
20
19
|
import beautify from "js-beautify";
|
|
21
20
|
import pug from "pug";
|
|
22
21
|
import untildify from "untildify";
|
|
@@ -26,12 +25,9 @@ function getTemplatePath(templateFileName, config2) {
|
|
|
26
25
|
fullTemplateFileName += "_full";
|
|
27
26
|
}
|
|
28
27
|
if (config2.templatePath !== void 0) {
|
|
29
|
-
return
|
|
30
|
-
untildify(config2.templatePath),
|
|
31
|
-
`${fullTemplateFileName}.pug`
|
|
32
|
-
);
|
|
28
|
+
return join(untildify(config2.templatePath), `${fullTemplateFileName}.pug`);
|
|
33
29
|
}
|
|
34
|
-
return
|
|
30
|
+
return join(
|
|
35
31
|
fileURLToPath(import.meta.url),
|
|
36
32
|
"../../../views/widget",
|
|
37
33
|
`${fullTemplateFileName}.pug`
|
|
@@ -149,12 +145,7 @@ function getStopsForDirection(route, direction, config2) {
|
|
|
149
145
|
[]
|
|
150
146
|
);
|
|
151
147
|
deduplicatedStopIds.pop();
|
|
152
|
-
const stopFields = [
|
|
153
|
-
"stop_id",
|
|
154
|
-
"stop_name",
|
|
155
|
-
"stop_code",
|
|
156
|
-
"parent_station"
|
|
157
|
-
];
|
|
148
|
+
const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
|
|
158
149
|
if (config2.includeCoordinates) {
|
|
159
150
|
stopFields.push("stop_lat", "stop_lon");
|
|
160
151
|
}
|
|
@@ -165,7 +156,7 @@ function getStopsForDirection(route, direction, config2) {
|
|
|
165
156
|
}
|
|
166
157
|
function generateTransitDeparturesWidgetHtml(config2) {
|
|
167
158
|
i18n.configure({
|
|
168
|
-
directory:
|
|
159
|
+
directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
|
|
169
160
|
defaultLocale: config2.locale,
|
|
170
161
|
updateFiles: false
|
|
171
162
|
});
|
|
@@ -184,7 +175,7 @@ function generateTransitDeparturesWidgetJson(config2) {
|
|
|
184
175
|
route.route_full_name = formatRouteName(route);
|
|
185
176
|
const directions = getDirectionsForRoute(route, config2);
|
|
186
177
|
if (directions.length === 0) {
|
|
187
|
-
|
|
178
|
+
logWarning(config2)(
|
|
188
179
|
`route_id ${route.route_id} has no directions - skipping`
|
|
189
180
|
);
|
|
190
181
|
continue;
|
|
@@ -251,7 +242,9 @@ function setDefaultConfig(initialConfig) {
|
|
|
251
242
|
refreshIntervalSeconds: 20,
|
|
252
243
|
skipImport: false,
|
|
253
244
|
timeFormat: "12hour",
|
|
254
|
-
includeCoordinates: false
|
|
245
|
+
includeCoordinates: false,
|
|
246
|
+
overwriteExistingFiles: true,
|
|
247
|
+
verbose: true
|
|
255
248
|
};
|
|
256
249
|
return Object.assign(defaults, initialConfig);
|
|
257
250
|
}
|
|
@@ -292,17 +285,13 @@ var selectedConfig = JSON.parse(readFileSync(configPath).toString());
|
|
|
292
285
|
var config = setDefaultConfig(selectedConfig);
|
|
293
286
|
config.noHead = false;
|
|
294
287
|
config.assetPath = "/";
|
|
295
|
-
config.
|
|
296
|
-
config.logWarning = console.warn;
|
|
297
|
-
config.logError = console.error;
|
|
288
|
+
config.logFunction = console.log;
|
|
298
289
|
try {
|
|
299
290
|
openDb2(config);
|
|
300
291
|
} catch (error) {
|
|
301
|
-
|
|
302
|
-
config.
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
}
|
|
292
|
+
console.error(
|
|
293
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and import GTFS before running this app.`
|
|
294
|
+
);
|
|
306
295
|
throw error;
|
|
307
296
|
}
|
|
308
297
|
router.get("/", async (request, response, next) => {
|
|
@@ -329,11 +318,11 @@ router.get("/data/stops.json", async (request, response, next) => {
|
|
|
329
318
|
next(error);
|
|
330
319
|
}
|
|
331
320
|
});
|
|
332
|
-
app.set("views",
|
|
321
|
+
app.set("views", path.join(fileURLToPath3(import.meta.url), "../../../views"));
|
|
333
322
|
app.set("view engine", "pug");
|
|
334
323
|
app.use(logger("dev"));
|
|
335
324
|
app.use(
|
|
336
|
-
express.static(
|
|
325
|
+
express.static(path.join(fileURLToPath3(import.meta.url), "../../../public"))
|
|
337
326
|
);
|
|
338
327
|
app.use("/", router);
|
|
339
328
|
app.set("port", process.env.PORT || 3e3);
|
package/dist/app/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\n\nimport express, { Router } from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\n\nconst argv = yargs(process.argv)\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .parseSync()\n\nconst app = express()\nconst router = Router()\n\nconst configPath = (argv.configPath ||\n new URL('../../config.json', import.meta.url)) as string\n\nconst selectedConfig = JSON.parse(readFileSync(configPath).toString())\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.log = console.log\nconfig.logWarning = console.warn\nconfig.logError = console.error\n\ntry {\n openDb(config)\n} catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n}\n\n/*\n * Show the transit departures widget\n */\nrouter.get('/', async (request, response, next) => {\n try {\n const html = await generateTransitDeparturesWidgetHtml(config)\n response.send(html)\n } catch (error) {\n next(error)\n }\n})\n\n/*\n * Provide data\n */\nrouter.get('/data/routes.json', async (request, response, next) => {\n try {\n const { routes } = await generateTransitDeparturesWidgetJson(config)\n response.json(routes)\n } catch (error) {\n next(error)\n }\n})\n\nrouter.get('/data/stops.json', async (request, response, next) => {\n try {\n const { stops } = await generateTransitDeparturesWidgetJson(config)\n response.json(stops)\n } catch (error) {\n next(error)\n }\n})\n\napp.set('views', path.join(fileURLToPath(import.meta.url), '../../../views'))\napp.set('view engine', 'pug')\n\napp.use(logger('dev'))\napp.use(\n express.static(path.join(fileURLToPath(import.meta.url), '../../../public')),\n)\n\napp.use('/', router)\napp.set('port', process.env.PORT || 3000)\n\nconst server = app.listen(app.get('port'), () => {\n console.log(`Express server listening on port ${app.get('port')}`)\n})\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: IConfig) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n path.resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n"],"mappings":"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","names":["path","fileURLToPath","openDb","fileURLToPath","config","config","config","stoptimes","fileURLToPath","openDb","path","fileURLToPath"]}
|
|
1
|
+
{"version":3,"sources":["../../src/app/index.ts","../../src/lib/utils.ts","../../src/lib/file-utils.ts"],"sourcesContent":["import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { readFileSync } from 'node:fs'\nimport yargs from 'yargs'\nimport { openDb } from 'gtfs'\n\nimport express, { Router } from 'express'\nimport logger from 'morgan'\n\nimport {\n setDefaultConfig,\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n} from '../lib/utils.ts'\n\nconst argv = yargs(process.argv)\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .parseSync()\n\nconst app = express()\nconst router = Router()\n\nconst configPath = (argv.configPath ||\n new URL('../../config.json', import.meta.url)) as string\n\nconst selectedConfig = JSON.parse(readFileSync(configPath).toString())\n\nconst config = setDefaultConfig(selectedConfig)\n// Override noHead config option so full HTML pages are generated\nconfig.noHead = false\nconfig.assetPath = '/'\nconfig.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"]}
|
|
@@ -6,17 +6,16 @@ import { hideBin } from "yargs/helpers";
|
|
|
6
6
|
import PrettyError from "pretty-error";
|
|
7
7
|
|
|
8
8
|
// src/lib/file-utils.ts
|
|
9
|
-
import
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import {
|
|
12
|
-
import copydir from "copy-dir";
|
|
11
|
+
import { access, cp, mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
13
12
|
import beautify from "js-beautify";
|
|
14
13
|
import pug from "pug";
|
|
15
14
|
import untildify from "untildify";
|
|
16
15
|
async function getConfig(argv2) {
|
|
17
16
|
try {
|
|
18
17
|
const data = await readFile(
|
|
19
|
-
|
|
18
|
+
resolve(untildify(argv2.configPath)),
|
|
20
19
|
"utf8"
|
|
21
20
|
).catch((error) => {
|
|
22
21
|
console.error(
|
|
@@ -40,44 +39,71 @@ async function getConfig(argv2) {
|
|
|
40
39
|
throw error;
|
|
41
40
|
}
|
|
42
41
|
}
|
|
42
|
+
function getPathToViewsFolder(config) {
|
|
43
|
+
if (config.templatePath) {
|
|
44
|
+
return untildify(config.templatePath);
|
|
45
|
+
}
|
|
46
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
let viewsFolderPath;
|
|
48
|
+
if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
|
|
49
|
+
viewsFolderPath = resolve(__dirname, "../../views/widget");
|
|
50
|
+
} else if (__dirname.endsWith("/dist")) {
|
|
51
|
+
viewsFolderPath = resolve(__dirname, "../views/widget");
|
|
52
|
+
} else {
|
|
53
|
+
viewsFolderPath = resolve(__dirname, "views/widget");
|
|
54
|
+
}
|
|
55
|
+
return viewsFolderPath;
|
|
56
|
+
}
|
|
43
57
|
function getTemplatePath(templateFileName, config) {
|
|
44
58
|
let fullTemplateFileName = templateFileName;
|
|
45
59
|
if (config.noHead !== true) {
|
|
46
60
|
fullTemplateFileName += "_full";
|
|
47
61
|
}
|
|
48
62
|
if (config.templatePath !== void 0) {
|
|
49
|
-
return
|
|
50
|
-
untildify(config.templatePath),
|
|
51
|
-
`${fullTemplateFileName}.pug`
|
|
52
|
-
);
|
|
63
|
+
return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
|
|
53
64
|
}
|
|
54
|
-
return
|
|
65
|
+
return join(
|
|
55
66
|
fileURLToPath(import.meta.url),
|
|
56
67
|
"../../../views/widget",
|
|
57
68
|
`${fullTemplateFileName}.pug`
|
|
58
69
|
);
|
|
59
70
|
}
|
|
60
|
-
async function prepDirectory(
|
|
61
|
-
await rm(exportPath, { recursive: true, force: true });
|
|
71
|
+
async function prepDirectory(outputPath, config) {
|
|
62
72
|
try {
|
|
63
|
-
await
|
|
73
|
+
await access(outputPath);
|
|
64
74
|
} catch (error) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
try {
|
|
76
|
+
await mkdir(outputPath, { recursive: true });
|
|
77
|
+
await mkdir(join(outputPath, "data"));
|
|
78
|
+
} catch (error2) {
|
|
79
|
+
if (error2?.code === "ENOENT") {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Unable to write to ${outputPath}. Try running this command from a writable directory.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
throw error2;
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
}
|
|
87
|
+
const files = await readdir(outputPath);
|
|
88
|
+
if (config.overwriteExistingFiles === false && files.length > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Output directory ${outputPath} is not empty. Please specify an empty directory.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (config.overwriteExistingFiles === true) {
|
|
94
|
+
await rm(join(outputPath, "*"), { recursive: true, force: true });
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
|
-
function copyStaticAssets(
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
async function copyStaticAssets(config, outputPath) {
|
|
98
|
+
const viewsFolderPath = getPathToViewsFolder(config);
|
|
99
|
+
const foldersToCopy = ["css", "js", "img"];
|
|
100
|
+
for (const folder of foldersToCopy) {
|
|
101
|
+
if (await access(join(viewsFolderPath, folder)).then(() => true).catch(() => false)) {
|
|
102
|
+
await cp(join(viewsFolderPath, folder), join(outputPath, folder), {
|
|
103
|
+
recursive: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
81
107
|
}
|
|
82
108
|
async function renderFile(templateFileName, templateVars, config) {
|
|
83
109
|
const templatePath = getTemplatePath(templateFileName, config);
|
|
@@ -89,7 +115,7 @@ async function renderFile(templateFileName, templateVars, config) {
|
|
|
89
115
|
}
|
|
90
116
|
|
|
91
117
|
// src/lib/log-utils.ts
|
|
92
|
-
import
|
|
118
|
+
import { clearLine, cursorTo } from "node:readline";
|
|
93
119
|
import { noop } from "lodash-es";
|
|
94
120
|
import * as colors from "yoctocolors";
|
|
95
121
|
function log(config) {
|
|
@@ -100,25 +126,15 @@ function log(config) {
|
|
|
100
126
|
return config.logFunction;
|
|
101
127
|
}
|
|
102
128
|
return (text, overwrite) => {
|
|
103
|
-
if (overwrite === true) {
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
if (overwrite === true && process.stdout.isTTY) {
|
|
130
|
+
clearLine(process.stdout, 0);
|
|
131
|
+
cursorTo(process.stdout, 0);
|
|
106
132
|
} else {
|
|
107
133
|
process.stdout.write("\n");
|
|
108
134
|
}
|
|
109
135
|
process.stdout.write(text);
|
|
110
136
|
};
|
|
111
137
|
}
|
|
112
|
-
function logWarning(config) {
|
|
113
|
-
if (config.logFunction) {
|
|
114
|
-
return config.logFunction;
|
|
115
|
-
}
|
|
116
|
-
return (text) => {
|
|
117
|
-
process.stdout.write(`
|
|
118
|
-
${formatWarning(text)}
|
|
119
|
-
`);
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
138
|
function logError(config) {
|
|
123
139
|
if (config.logFunction) {
|
|
124
140
|
return config.logFunction;
|
|
@@ -129,10 +145,6 @@ ${formatError(text)}
|
|
|
129
145
|
`);
|
|
130
146
|
};
|
|
131
147
|
}
|
|
132
|
-
function formatWarning(text) {
|
|
133
|
-
const warningMessage = `${colors.underline("Warning")}: ${text}`;
|
|
134
|
-
return colors.yellow(warningMessage);
|
|
135
|
-
}
|
|
136
148
|
function formatError(error) {
|
|
137
149
|
const messageText = error instanceof Error ? error.message : error;
|
|
138
150
|
const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
|
|
@@ -143,16 +155,17 @@ function formatError(error) {
|
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
// src/lib/transit-departures-widget.ts
|
|
146
|
-
import
|
|
158
|
+
import path from "path";
|
|
147
159
|
import { clone, omit } from "lodash-es";
|
|
148
160
|
import { writeFile } from "node:fs/promises";
|
|
149
161
|
import { importGtfs, openDb as openDb2 } from "gtfs";
|
|
150
162
|
import sanitize from "sanitize-filename";
|
|
151
163
|
import Timer from "timer-machine";
|
|
164
|
+
import untildify2 from "untildify";
|
|
152
165
|
|
|
153
166
|
// src/lib/utils.ts
|
|
154
167
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
155
|
-
import { dirname, join } from "path";
|
|
168
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
156
169
|
import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
157
170
|
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
158
171
|
import sqlString from "sqlstring-sqlite";
|
|
@@ -257,12 +270,7 @@ function getStopsForDirection(route, direction, config) {
|
|
|
257
270
|
[]
|
|
258
271
|
);
|
|
259
272
|
deduplicatedStopIds.pop();
|
|
260
|
-
const stopFields = [
|
|
261
|
-
"stop_id",
|
|
262
|
-
"stop_name",
|
|
263
|
-
"stop_code",
|
|
264
|
-
"parent_station"
|
|
265
|
-
];
|
|
273
|
+
const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
|
|
266
274
|
if (config.includeCoordinates) {
|
|
267
275
|
stopFields.push("stop_lat", "stop_lon");
|
|
268
276
|
}
|
|
@@ -273,7 +281,7 @@ function getStopsForDirection(route, direction, config) {
|
|
|
273
281
|
}
|
|
274
282
|
function generateTransitDeparturesWidgetHtml(config) {
|
|
275
283
|
i18n.configure({
|
|
276
|
-
directory:
|
|
284
|
+
directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
|
|
277
285
|
defaultLocale: config.locale,
|
|
278
286
|
updateFiles: false
|
|
279
287
|
});
|
|
@@ -292,7 +300,7 @@ function generateTransitDeparturesWidgetJson(config) {
|
|
|
292
300
|
route.route_full_name = formatRouteName(route);
|
|
293
301
|
const directions = getDirectionsForRoute(route, config);
|
|
294
302
|
if (directions.length === 0) {
|
|
295
|
-
|
|
303
|
+
logWarning(config)(
|
|
296
304
|
`route_id ${route.route_id} has no directions - skipping`
|
|
297
305
|
);
|
|
298
306
|
continue;
|
|
@@ -359,7 +367,9 @@ function setDefaultConfig(initialConfig) {
|
|
|
359
367
|
refreshIntervalSeconds: 20,
|
|
360
368
|
skipImport: false,
|
|
361
369
|
timeFormat: "12hour",
|
|
362
|
-
includeCoordinates: false
|
|
370
|
+
includeCoordinates: false,
|
|
371
|
+
overwriteExistingFiles: true,
|
|
372
|
+
verbose: true
|
|
363
373
|
};
|
|
364
374
|
return Object.assign(defaults, initialConfig);
|
|
365
375
|
}
|
|
@@ -389,14 +399,11 @@ function formatWhereClauses(query) {
|
|
|
389
399
|
// src/lib/transit-departures-widget.ts
|
|
390
400
|
async function transitDeparturesWidget(initialConfig) {
|
|
391
401
|
const config = setDefaultConfig(initialConfig);
|
|
392
|
-
config.log = log(config);
|
|
393
|
-
config.logWarning = logWarning(config);
|
|
394
|
-
config.logError = logError(config);
|
|
395
402
|
try {
|
|
396
403
|
openDb2(config);
|
|
397
404
|
} catch (error) {
|
|
398
405
|
if (error?.code === "SQLITE_CANTOPEN") {
|
|
399
|
-
|
|
406
|
+
logError(config)(
|
|
400
407
|
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
401
408
|
);
|
|
402
409
|
}
|
|
@@ -406,8 +413,8 @@ async function transitDeparturesWidget(initialConfig) {
|
|
|
406
413
|
throw new Error("No agency defined in `config.json`");
|
|
407
414
|
}
|
|
408
415
|
const timer = new Timer();
|
|
409
|
-
const agencyKey = config.agency.agency_key;
|
|
410
|
-
const
|
|
416
|
+
const agencyKey = config.agency.agency_key ?? "unknown";
|
|
417
|
+
const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize(agencyKey));
|
|
411
418
|
timer.start();
|
|
412
419
|
if (!config.skipImport) {
|
|
413
420
|
const gtfsImportConfig = {
|
|
@@ -422,30 +429,29 @@ async function transitDeparturesWidget(initialConfig) {
|
|
|
422
429
|
};
|
|
423
430
|
await importGtfs(gtfsImportConfig);
|
|
424
431
|
}
|
|
425
|
-
await prepDirectory(
|
|
426
|
-
await prepDirectory(path2.join(exportPath, "data"));
|
|
432
|
+
await prepDirectory(outputPath, config);
|
|
427
433
|
if (config.noHead !== true) {
|
|
428
|
-
copyStaticAssets(
|
|
434
|
+
await copyStaticAssets(config, outputPath);
|
|
429
435
|
}
|
|
430
|
-
|
|
436
|
+
log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`);
|
|
431
437
|
config.assetPath = "";
|
|
432
438
|
const { routes, stops } = generateTransitDeparturesWidgetJson(config);
|
|
433
439
|
await writeFile(
|
|
434
|
-
|
|
440
|
+
path.join(outputPath, "data", "routes.json"),
|
|
435
441
|
JSON.stringify(routes, null, 2)
|
|
436
442
|
);
|
|
437
443
|
await writeFile(
|
|
438
|
-
|
|
444
|
+
path.join(outputPath, "data", "stops.json"),
|
|
439
445
|
JSON.stringify(stops, null, 2)
|
|
440
446
|
);
|
|
441
447
|
const html = await generateTransitDeparturesWidgetHtml(config);
|
|
442
|
-
await writeFile(
|
|
448
|
+
await writeFile(path.join(outputPath, "index.html"), html);
|
|
443
449
|
timer.stop();
|
|
444
|
-
|
|
445
|
-
`${agencyKey}: Transit Departures Widget HTML created at ${
|
|
450
|
+
log(config)(
|
|
451
|
+
`${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`
|
|
446
452
|
);
|
|
447
453
|
const seconds = Math.round(timer.time() / 1e3);
|
|
448
|
-
|
|
454
|
+
log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`);
|
|
449
455
|
}
|
|
450
456
|
var transit_departures_widget_default = transitDeparturesWidget;
|
|
451
457
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/utils.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport PrettyError from 'pretty-error'\n\nimport { getConfig } from '../lib/file-utils.ts'\nimport { formatError } from '../lib/log-utils.ts'\nimport transitDeparturesWidget from '../index.ts'\n\nconst pe = new PrettyError()\n\nconst argv = yargs(hideBin(process.argv))\n .usage('Usage: $0 --config ./config.json')\n .help()\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .option('s', {\n alias: 'skipImport',\n describe: 'Don’t import GTFS file.',\n type: 'boolean',\n })\n .default('skipImport', undefined)\n .parseSync()\n\nconst handleError = (error: any) => {\n const text = error || 'Unknown Error'\n process.stdout.write(`\\n${formatError(text)}\\n`)\n console.error(pe.render(error))\n process.exit(1)\n}\n\nconst setupImport = async () => {\n const config = await getConfig(argv)\n await transitDeparturesWidget(config)\n process.exit()\n}\n\nsetupImport().catch(handleError)\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n path.resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function formatError(error: any) {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n","import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: IConfig) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n"],"mappings":"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","names":["argv","path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/bin/transit-departures-widget.ts","../../src/lib/file-utils.ts","../../src/lib/log-utils.ts","../../src/lib/transit-departures-widget.ts","../../src/lib/utils.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\nimport PrettyError from 'pretty-error'\n\nimport { getConfig } from '../lib/file-utils.ts'\nimport { formatError } from '../lib/log-utils.ts'\nimport transitDeparturesWidget from '../index.ts'\n\nconst pe = new PrettyError()\n\nconst argv = yargs(hideBin(process.argv))\n .usage('Usage: $0 --config ./config.json')\n .help()\n .option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n })\n .option('s', {\n alias: 'skipImport',\n describe: 'Don’t import GTFS file.',\n type: 'boolean',\n })\n .default('skipImport', undefined)\n .parseSync()\n\nconst handleError = (error: any) => {\n const text = error || 'Unknown Error'\n process.stdout.write(`\\n${formatError(text)}\\n`)\n console.error(pe.render(error))\n process.exit(1)\n}\n\nconst setupImport = async () => {\n const config = await getConfig(argv)\n await transitDeparturesWidget(config)\n process.exit()\n}\n\nsetupImport().catch(handleError)\n","import { 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"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface
|
|
1
|
+
interface Config {
|
|
2
2
|
agency: {
|
|
3
3
|
agency_key: string;
|
|
4
4
|
gtfs_static_path?: string;
|
|
@@ -14,16 +14,17 @@ interface IConfig {
|
|
|
14
14
|
locale?: string;
|
|
15
15
|
includeCoordinates?: boolean;
|
|
16
16
|
noHead?: boolean;
|
|
17
|
+
outputPath?: string;
|
|
18
|
+
overwriteExistingFiles?: boolean;
|
|
17
19
|
refreshIntervalSeconds?: number;
|
|
18
20
|
skipImport?: boolean;
|
|
19
21
|
sqlitePath?: string;
|
|
20
22
|
templatePath?: string;
|
|
21
23
|
timeFormat?: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
logError: (text: string) => void;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
logFunction?: (text: string) => void;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
declare function transitDeparturesWidget(initialConfig:
|
|
28
|
+
declare function transitDeparturesWidget(initialConfig: Config): Promise<void>;
|
|
28
29
|
|
|
29
30
|
export { transitDeparturesWidget as default };
|
package/dist/index.js
CHANGED
|
@@ -1,57 +1,84 @@
|
|
|
1
1
|
// src/lib/transit-departures-widget.ts
|
|
2
|
-
import
|
|
2
|
+
import path from "path";
|
|
3
3
|
import { clone, omit } from "lodash-es";
|
|
4
4
|
import { writeFile } from "node:fs/promises";
|
|
5
5
|
import { importGtfs, openDb as openDb2 } from "gtfs";
|
|
6
6
|
import sanitize from "sanitize-filename";
|
|
7
7
|
import Timer from "timer-machine";
|
|
8
|
+
import untildify2 from "untildify";
|
|
8
9
|
|
|
9
10
|
// src/lib/file-utils.ts
|
|
10
|
-
import
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import {
|
|
13
|
-
import copydir from "copy-dir";
|
|
13
|
+
import { access, cp, mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
14
14
|
import beautify from "js-beautify";
|
|
15
15
|
import pug from "pug";
|
|
16
16
|
import untildify from "untildify";
|
|
17
|
+
function getPathToViewsFolder(config) {
|
|
18
|
+
if (config.templatePath) {
|
|
19
|
+
return untildify(config.templatePath);
|
|
20
|
+
}
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
let viewsFolderPath;
|
|
23
|
+
if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
|
|
24
|
+
viewsFolderPath = resolve(__dirname, "../../views/widget");
|
|
25
|
+
} else if (__dirname.endsWith("/dist")) {
|
|
26
|
+
viewsFolderPath = resolve(__dirname, "../views/widget");
|
|
27
|
+
} else {
|
|
28
|
+
viewsFolderPath = resolve(__dirname, "views/widget");
|
|
29
|
+
}
|
|
30
|
+
return viewsFolderPath;
|
|
31
|
+
}
|
|
17
32
|
function getTemplatePath(templateFileName, config) {
|
|
18
33
|
let fullTemplateFileName = templateFileName;
|
|
19
34
|
if (config.noHead !== true) {
|
|
20
35
|
fullTemplateFileName += "_full";
|
|
21
36
|
}
|
|
22
37
|
if (config.templatePath !== void 0) {
|
|
23
|
-
return
|
|
24
|
-
untildify(config.templatePath),
|
|
25
|
-
`${fullTemplateFileName}.pug`
|
|
26
|
-
);
|
|
38
|
+
return join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
|
|
27
39
|
}
|
|
28
|
-
return
|
|
40
|
+
return join(
|
|
29
41
|
fileURLToPath(import.meta.url),
|
|
30
42
|
"../../../views/widget",
|
|
31
43
|
`${fullTemplateFileName}.pug`
|
|
32
44
|
);
|
|
33
45
|
}
|
|
34
|
-
async function prepDirectory(
|
|
35
|
-
await rm(exportPath, { recursive: true, force: true });
|
|
46
|
+
async function prepDirectory(outputPath, config) {
|
|
36
47
|
try {
|
|
37
|
-
await
|
|
48
|
+
await access(outputPath);
|
|
38
49
|
} catch (error) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(outputPath, { recursive: true });
|
|
52
|
+
await mkdir(join(outputPath, "data"));
|
|
53
|
+
} catch (error2) {
|
|
54
|
+
if (error2?.code === "ENOENT") {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Unable to write to ${outputPath}. Try running this command from a writable directory.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
throw error2;
|
|
43
60
|
}
|
|
44
|
-
|
|
61
|
+
}
|
|
62
|
+
const files = await readdir(outputPath);
|
|
63
|
+
if (config.overwriteExistingFiles === false && files.length > 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Output directory ${outputPath} is not empty. Please specify an empty directory.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (config.overwriteExistingFiles === true) {
|
|
69
|
+
await rm(join(outputPath, "*"), { recursive: true, force: true });
|
|
45
70
|
}
|
|
46
71
|
}
|
|
47
|
-
function copyStaticAssets(
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
async function copyStaticAssets(config, outputPath) {
|
|
73
|
+
const viewsFolderPath = getPathToViewsFolder(config);
|
|
74
|
+
const foldersToCopy = ["css", "js", "img"];
|
|
75
|
+
for (const folder of foldersToCopy) {
|
|
76
|
+
if (await access(join(viewsFolderPath, folder)).then(() => true).catch(() => false)) {
|
|
77
|
+
await cp(join(viewsFolderPath, folder), join(outputPath, folder), {
|
|
78
|
+
recursive: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
55
82
|
}
|
|
56
83
|
async function renderFile(templateFileName, templateVars, config) {
|
|
57
84
|
const templatePath = getTemplatePath(templateFileName, config);
|
|
@@ -63,7 +90,7 @@ async function renderFile(templateFileName, templateVars, config) {
|
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
// src/lib/log-utils.ts
|
|
66
|
-
import
|
|
93
|
+
import { clearLine, cursorTo } from "node:readline";
|
|
67
94
|
import { noop } from "lodash-es";
|
|
68
95
|
import * as colors from "yoctocolors";
|
|
69
96
|
function log(config) {
|
|
@@ -74,25 +101,15 @@ function log(config) {
|
|
|
74
101
|
return config.logFunction;
|
|
75
102
|
}
|
|
76
103
|
return (text, overwrite) => {
|
|
77
|
-
if (overwrite === true) {
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
if (overwrite === true && process.stdout.isTTY) {
|
|
105
|
+
clearLine(process.stdout, 0);
|
|
106
|
+
cursorTo(process.stdout, 0);
|
|
80
107
|
} else {
|
|
81
108
|
process.stdout.write("\n");
|
|
82
109
|
}
|
|
83
110
|
process.stdout.write(text);
|
|
84
111
|
};
|
|
85
112
|
}
|
|
86
|
-
function logWarning(config) {
|
|
87
|
-
if (config.logFunction) {
|
|
88
|
-
return config.logFunction;
|
|
89
|
-
}
|
|
90
|
-
return (text) => {
|
|
91
|
-
process.stdout.write(`
|
|
92
|
-
${formatWarning(text)}
|
|
93
|
-
`);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
113
|
function logError(config) {
|
|
97
114
|
if (config.logFunction) {
|
|
98
115
|
return config.logFunction;
|
|
@@ -103,10 +120,6 @@ ${formatError(text)}
|
|
|
103
120
|
`);
|
|
104
121
|
};
|
|
105
122
|
}
|
|
106
|
-
function formatWarning(text) {
|
|
107
|
-
const warningMessage = `${colors.underline("Warning")}: ${text}`;
|
|
108
|
-
return colors.yellow(warningMessage);
|
|
109
|
-
}
|
|
110
123
|
function formatError(error) {
|
|
111
124
|
const messageText = error instanceof Error ? error.message : error;
|
|
112
125
|
const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
|
|
@@ -118,7 +131,7 @@ function formatError(error) {
|
|
|
118
131
|
|
|
119
132
|
// src/lib/utils.ts
|
|
120
133
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
121
|
-
import { dirname, join } from "path";
|
|
134
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
122
135
|
import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
123
136
|
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
124
137
|
import sqlString from "sqlstring-sqlite";
|
|
@@ -223,12 +236,7 @@ function getStopsForDirection(route, direction, config) {
|
|
|
223
236
|
[]
|
|
224
237
|
);
|
|
225
238
|
deduplicatedStopIds.pop();
|
|
226
|
-
const stopFields = [
|
|
227
|
-
"stop_id",
|
|
228
|
-
"stop_name",
|
|
229
|
-
"stop_code",
|
|
230
|
-
"parent_station"
|
|
231
|
-
];
|
|
239
|
+
const stopFields = ["stop_id", "stop_name", "stop_code", "parent_station"];
|
|
232
240
|
if (config.includeCoordinates) {
|
|
233
241
|
stopFields.push("stop_lat", "stop_lon");
|
|
234
242
|
}
|
|
@@ -239,7 +247,7 @@ function getStopsForDirection(route, direction, config) {
|
|
|
239
247
|
}
|
|
240
248
|
function generateTransitDeparturesWidgetHtml(config) {
|
|
241
249
|
i18n.configure({
|
|
242
|
-
directory:
|
|
250
|
+
directory: join2(dirname2(fileURLToPath2(import.meta.url)), "../../locales"),
|
|
243
251
|
defaultLocale: config.locale,
|
|
244
252
|
updateFiles: false
|
|
245
253
|
});
|
|
@@ -258,7 +266,7 @@ function generateTransitDeparturesWidgetJson(config) {
|
|
|
258
266
|
route.route_full_name = formatRouteName(route);
|
|
259
267
|
const directions = getDirectionsForRoute(route, config);
|
|
260
268
|
if (directions.length === 0) {
|
|
261
|
-
|
|
269
|
+
logWarning(config)(
|
|
262
270
|
`route_id ${route.route_id} has no directions - skipping`
|
|
263
271
|
);
|
|
264
272
|
continue;
|
|
@@ -325,7 +333,9 @@ function setDefaultConfig(initialConfig) {
|
|
|
325
333
|
refreshIntervalSeconds: 20,
|
|
326
334
|
skipImport: false,
|
|
327
335
|
timeFormat: "12hour",
|
|
328
|
-
includeCoordinates: false
|
|
336
|
+
includeCoordinates: false,
|
|
337
|
+
overwriteExistingFiles: true,
|
|
338
|
+
verbose: true
|
|
329
339
|
};
|
|
330
340
|
return Object.assign(defaults, initialConfig);
|
|
331
341
|
}
|
|
@@ -355,14 +365,11 @@ function formatWhereClauses(query) {
|
|
|
355
365
|
// src/lib/transit-departures-widget.ts
|
|
356
366
|
async function transitDeparturesWidget(initialConfig) {
|
|
357
367
|
const config = setDefaultConfig(initialConfig);
|
|
358
|
-
config.log = log(config);
|
|
359
|
-
config.logWarning = logWarning(config);
|
|
360
|
-
config.logError = logError(config);
|
|
361
368
|
try {
|
|
362
369
|
openDb2(config);
|
|
363
370
|
} catch (error) {
|
|
364
371
|
if (error?.code === "SQLITE_CANTOPEN") {
|
|
365
|
-
|
|
372
|
+
logError(config)(
|
|
366
373
|
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
367
374
|
);
|
|
368
375
|
}
|
|
@@ -372,8 +379,8 @@ async function transitDeparturesWidget(initialConfig) {
|
|
|
372
379
|
throw new Error("No agency defined in `config.json`");
|
|
373
380
|
}
|
|
374
381
|
const timer = new Timer();
|
|
375
|
-
const agencyKey = config.agency.agency_key;
|
|
376
|
-
const
|
|
382
|
+
const agencyKey = config.agency.agency_key ?? "unknown";
|
|
383
|
+
const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize(agencyKey));
|
|
377
384
|
timer.start();
|
|
378
385
|
if (!config.skipImport) {
|
|
379
386
|
const gtfsImportConfig = {
|
|
@@ -388,30 +395,29 @@ async function transitDeparturesWidget(initialConfig) {
|
|
|
388
395
|
};
|
|
389
396
|
await importGtfs(gtfsImportConfig);
|
|
390
397
|
}
|
|
391
|
-
await prepDirectory(
|
|
392
|
-
await prepDirectory(path2.join(exportPath, "data"));
|
|
398
|
+
await prepDirectory(outputPath, config);
|
|
393
399
|
if (config.noHead !== true) {
|
|
394
|
-
copyStaticAssets(
|
|
400
|
+
await copyStaticAssets(config, outputPath);
|
|
395
401
|
}
|
|
396
|
-
|
|
402
|
+
log(config)(`${agencyKey}: Generating Transit Departures Widget HTML`);
|
|
397
403
|
config.assetPath = "";
|
|
398
404
|
const { routes, stops } = generateTransitDeparturesWidgetJson(config);
|
|
399
405
|
await writeFile(
|
|
400
|
-
|
|
406
|
+
path.join(outputPath, "data", "routes.json"),
|
|
401
407
|
JSON.stringify(routes, null, 2)
|
|
402
408
|
);
|
|
403
409
|
await writeFile(
|
|
404
|
-
|
|
410
|
+
path.join(outputPath, "data", "stops.json"),
|
|
405
411
|
JSON.stringify(stops, null, 2)
|
|
406
412
|
);
|
|
407
413
|
const html = await generateTransitDeparturesWidgetHtml(config);
|
|
408
|
-
await writeFile(
|
|
414
|
+
await writeFile(path.join(outputPath, "index.html"), html);
|
|
409
415
|
timer.stop();
|
|
410
|
-
|
|
411
|
-
`${agencyKey}: Transit Departures Widget HTML created at ${
|
|
416
|
+
log(config)(
|
|
417
|
+
`${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`
|
|
412
418
|
);
|
|
413
419
|
const seconds = Math.round(timer.time() / 1e3);
|
|
414
|
-
|
|
420
|
+
log(config)(`${agencyKey}: HTML generation required ${seconds} seconds`);
|
|
415
421
|
}
|
|
416
422
|
var transit_departures_widget_default = transitDeparturesWidget;
|
|
417
423
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/log-utils.ts","../src/lib/utils.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\n\nimport { copyStaticAssets, prepDirectory } from './file-utils.ts'\nimport { log, logWarning, logError } from './log-utils.ts'\nimport {\n generateTransitDeparturesWidgetHtml,\n generateTransitDeparturesWidgetJson,\n setDefaultConfig,\n} from './utils.ts'\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Generate transit departures widget HTML from GTFS.\n */\nasync function transitDeparturesWidget(initialConfig: IConfig) {\n const config = setDefaultConfig(initialConfig)\n config.log = log(config)\n config.logWarning = logWarning(config)\n config.logError = logError(config)\n\n try {\n openDb(config)\n } catch (error: any) {\n if (error?.code === 'SQLITE_CANTOPEN') {\n config.logError(\n `Unable to open sqlite database \"${config.sqlitePath}\" defined as \\`sqlitePath\\` config.json. Ensure the parent directory exists or remove \\`sqlitePath\\` from config.json.`,\n )\n }\n\n throw error\n }\n\n if (!config.agency) {\n throw new Error('No agency defined in `config.json`')\n }\n\n const timer = new Timer()\n const agencyKey = config.agency.agency_key\n const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey))\n\n timer.start()\n\n if (!config.skipImport) {\n // Import GTFS\n const gtfsImportConfig = {\n ...clone(omit(config, 'agency')),\n agencies: [\n {\n agency_key: config.agency.agency_key,\n path: config.agency.gtfs_static_path,\n url: config.agency.gtfs_static_url,\n },\n ],\n }\n\n await importGtfs(gtfsImportConfig)\n }\n\n await prepDirectory(exportPath)\n await prepDirectory(path.join(exportPath, 'data'))\n\n if (config.noHead !== true) {\n copyStaticAssets(exportPath)\n }\n\n config.log(`${agencyKey}: Generating Transit Departures Widget HTML`)\n\n config.assetPath = ''\n\n // Generate JSON of routes and stops\n const { routes, stops } = generateTransitDeparturesWidgetJson(config)\n await writeFile(\n path.join(exportPath, 'data', 'routes.json'),\n JSON.stringify(routes, null, 2),\n )\n await writeFile(\n path.join(exportPath, 'data', 'stops.json'),\n JSON.stringify(stops, null, 2),\n )\n\n const html = await generateTransitDeparturesWidgetHtml(config)\n await writeFile(path.join(exportPath, 'index.html'), html)\n\n timer.stop()\n\n // Print stats\n config.log(\n `${agencyKey}: Transit Departures Widget HTML created at ${exportPath}`,\n )\n\n const seconds = Math.round(timer.time() / 1000)\n config.log(`${agencyKey}: HTML generation required ${seconds} seconds`)\n}\n\nexport default transitDeparturesWidget\n","import path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { readFile, rm, mkdir } from 'node:fs/promises'\nimport copydir from 'copy-dir'\nimport beautify from 'js-beautify'\nimport pug from 'pug'\nimport untildify from 'untildify'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Attempt to parse the specified config JSON file.\n */\nexport async function getConfig(argv) {\n try {\n const data = await readFile(\n path.resolve(untildify(argv.configPath)),\n 'utf8',\n ).catch((error) => {\n console.error(\n new Error(\n `Cannot find configuration file at \\`${argv.configPath}\\`. Use config-sample.json as a starting point, pass --configPath option`,\n ),\n )\n throw error\n })\n const config = JSON.parse(data)\n\n if (argv.skipImport === true) {\n config.skipImport = argv.skipImport\n }\n\n return config\n } catch (error) {\n console.error(\n new Error(\n `Cannot parse configuration file at \\`${argv.configPath}\\`. Check to ensure that it is valid JSON.`,\n ),\n )\n throw error\n }\n}\n\n/*\n * Get the full path of the template file for generating transit departures widget based on\n * config.\n */\nfunction getTemplatePath(templateFileName, config) {\n let fullTemplateFileName = templateFileName\n if (config.noHead !== true) {\n fullTemplateFileName += '_full'\n }\n\n if (config.templatePath !== undefined) {\n return path.join(\n untildify(config.templatePath),\n `${fullTemplateFileName}.pug`,\n )\n }\n\n return path.join(\n fileURLToPath(import.meta.url),\n '../../../views/widget',\n `${fullTemplateFileName}.pug`,\n )\n}\n\n/*\n * Prepare the specified directory for saving HTML widget by deleting everything.\n */\nexport async function prepDirectory(exportPath: string) {\n await rm(exportPath, { recursive: true, force: true })\n try {\n await mkdir(exportPath, { recursive: true })\n } catch (error: any) {\n if (error?.code === 'ENOENT') {\n throw new Error(\n `Unable to write to ${exportPath}. Try running this command from a writable directory.`,\n )\n }\n\n throw error\n }\n}\n\n/*\n * Copy needed CSS and JS to export path.\n */\nexport function copyStaticAssets(exportPath: string) {\n const staticAssetPath = path.join(\n fileURLToPath(import.meta.url),\n '../../../public',\n )\n copydir.sync(path.join(staticAssetPath, 'img'), path.join(exportPath, 'img'))\n copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'))\n copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'))\n}\n\n/*\n * Render the HTML based on the config.\n */\nexport async function renderFile(\n templateFileName: string,\n templateVars: any,\n config: IConfig,\n) {\n const templatePath = getTemplatePath(templateFileName, config)\n const html = await pug.renderFile(templatePath, templateVars)\n\n // Beautify HTML if setting is set\n if (config.beautify === true) {\n return beautify.html_beautify(html, { indent_size: 2 })\n }\n\n return html\n}\n","import readline from 'readline'\nimport { noop } from 'lodash-es'\nimport * as colors from 'yoctocolors'\n\nimport { IConfig } from '../types/global_interfaces.ts'\n\n/*\n * Returns a log function based on config settings\n */\nexport function log(config: IConfig) {\n if (config.verbose === false) {\n return noop\n }\n\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string, overwrite: boolean) => {\n if (overwrite === true) {\n readline.clearLine(process.stdout, 0)\n readline.cursorTo(process.stdout, 0)\n } else {\n process.stdout.write('\\n')\n }\n\n process.stdout.write(text)\n }\n}\n\n/*\n * Returns an warning log function based on config settings\n */\nexport function logWarning(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatWarning(text)}\\n`)\n }\n}\n\n/*\n * Returns an error log function based on config settings\n */\nexport function logError(config: IConfig) {\n if (config.logFunction) {\n return config.logFunction\n }\n\n return (text: string) => {\n process.stdout.write(`\\n${formatError(text)}\\n`)\n }\n}\n\n/*\n * Format console warning text\n */\nexport function formatWarning(text: string) {\n const warningMessage = `${colors.underline('Warning')}: ${text}`\n return colors.yellow(warningMessage)\n}\n\n/*\n * Format console error text\n */\nexport function formatError(error: any) {\n const messageText = error instanceof Error ? error.message : error\n const errorMessage = `${colors.underline('Error')}: ${messageText.replace(\n 'Error: ',\n '',\n )}`\n return colors.red(errorMessage)\n}\n","import { fileURLToPath } from 'url'\nimport { dirname, join } from 'path'\nimport { openDb, getDirections, getRoutes, getStops, getTrips } from 'gtfs'\nimport { groupBy, last, maxBy, size, sortBy, uniqBy } from 'lodash-es'\nimport { renderFile } from './file-utils.ts'\nimport sqlString from 'sqlstring-sqlite'\nimport toposort from 'toposort'\nimport i18n from 'i18n'\n\nimport { IConfig, SqlWhere, SqlValue } from '../types/global_interfaces.ts'\n\n/*\n * Get calendars for a specified date range\n */\nconst getCalendarsForDateRange = (config: IConfig) => {\n const db = openDb(config)\n let whereClause = ''\n const whereClauses = []\n\n if (config.endDate) {\n whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`)\n }\n\n if (config.startDate) {\n whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`)\n }\n\n if (whereClauses.length > 0) {\n whereClause = `WHERE ${whereClauses.join(' AND ')}`\n }\n\n return db.prepare(`SELECT * FROM calendar ${whereClause}`).all()\n}\n\n/*\n * Format a route name.\n */\nfunction formatRouteName(route) {\n let routeName = ''\n\n if (route.route_short_name !== null) {\n routeName += route.route_short_name\n }\n\n if (route.route_short_name !== null && route.route_long_name !== null) {\n routeName += ' - '\n }\n\n if (route.route_long_name !== null) {\n routeName += route.route_long_name\n }\n\n return routeName\n}\n\n/*\n * Get directions for a route\n */\nfunction getDirectionsForRoute(route: Record<string, string>, config: IConfig) {\n const db = openDb(config)\n\n // Lookup direction names from non-standard directions.txt file\n const directions = getDirections({ route_id: route.route_id }, [\n 'direction_id',\n 'direction',\n ])\n\n const calendars = getCalendarsForDateRange(config)\n\n // Else use the most common headsigns as directions from trips.txt file\n if (directions.length === 0) {\n const headsigns = db\n .prepare(\n `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars\n .map((calendar: Record<string, string>) => `'${calendar.service_id}'`)\n .join(', ')}) GROUP BY direction_id, trip_headsign`,\n )\n .all(route.route_id)\n\n for (const group of Object.values(groupBy(headsigns, 'direction_id'))) {\n const mostCommonHeadsign = maxBy(group, 'count')\n directions.push({\n direction_id: mostCommonHeadsign.direction_id,\n direction: i18n.__('To {{{headsign}}}', {\n headsign: mostCommonHeadsign.trip_headsign,\n }),\n })\n }\n }\n\n return directions\n}\n\n/*\n * Sort an array of stoptimes by stop_sequence using a directed graph\n */\nfunction sortStopIdsBySequence(stoptimes: Record<string, string>[]) {\n const stoptimesGroupedByTrip = groupBy(stoptimes, 'trip_id')\n\n // First, try using a directed graph to determine stop order.\n try {\n const stopGraph = []\n\n for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {\n const sortedStopIds = sortBy(tripStoptimes, 'stop_sequence').map(\n (stoptime) => stoptime.stop_id,\n )\n\n for (const [index, stopId] of sortedStopIds.entries()) {\n if (index === sortedStopIds.length - 1) {\n continue\n }\n\n stopGraph.push([stopId, sortedStopIds[index + 1]])\n }\n }\n\n return toposort(\n stopGraph as unknown as readonly [string, string | undefined][],\n )\n } catch {\n // Ignore errors and move to next strategy.\n }\n\n // Finally, fall back to using the stop order from the trip with the most stoptimes.\n const longestTripStoptimes = maxBy(\n Object.values(stoptimesGroupedByTrip),\n (stoptimes) => size(stoptimes),\n )\n\n return longestTripStoptimes.map((stoptime) => stoptime.stop_id)\n}\n\n/*\n * Get stops in order for a route and direction\n */\nfunction getStopsForDirection(route, direction, config: IConfig) {\n const db = openDb(config)\n const calendars = getCalendarsForDateRange(config)\n const whereClause = formatWhereClauses({\n direction_id: direction.direction_id,\n route_id: route.route_id,\n service_id: calendars.map((calendar) => calendar.service_id),\n })\n const stoptimes = db\n .prepare(\n `SELECT stop_id, stop_sequence, trip_id FROM stop_times WHERE trip_id IN (SELECT trip_id FROM trips ${whereClause}) ORDER BY stop_sequence ASC`,\n )\n .all()\n\n const sortedStopIds = sortStopIdsBySequence(stoptimes)\n\n const deduplicatedStopIds = sortedStopIds.reduce(\n (memo: string[], stopId: string) => {\n // Remove duplicated stop_ids in a row\n if (last(memo) !== stopId) {\n memo.push(stopId)\n }\n\n return memo\n },\n [],\n )\n\n // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n\n const stopFields = [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ]\n\n if (config.includeCoordinates) {\n stopFields.push('stop_lat', 'stop_lon')\n }\n\n const stops = getStops({ stop_id: deduplicatedStopIds }, stopFields)\n\n return deduplicatedStopIds.map((stopId: string) =>\n stops.find((stop) => stop.stop_id === stopId),\n )\n}\n\n/*\n * Generate HTML for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetHtml(config: IConfig) {\n i18n.configure({\n directory: join(dirname(fileURLToPath(import.meta.url)), '../../locales'),\n defaultLocale: config.locale,\n updateFiles: false,\n })\n\n const templateVars = {\n __: i18n.__,\n config,\n }\n return renderFile('widget', templateVars, config)\n}\n\n/*\n * Generate JSON of routes and stops for transit departures widget.\n */\nexport function generateTransitDeparturesWidgetJson(config: IConfig) {\n const routes = getRoutes()\n const stops = []\n const filteredRoutes = []\n const calendars = getCalendarsForDateRange(config)\n\n for (const route of routes) {\n route.route_full_name = formatRouteName(route)\n\n const directions = getDirectionsForRoute(route, config)\n\n // Filter out routes with no directions\n if (directions.length === 0) {\n config.logWarning(\n `route_id ${route.route_id} has no directions - skipping`,\n )\n continue\n }\n\n for (const direction of directions) {\n const directionStops = getStopsForDirection(route, direction, config)\n stops.push(...directionStops)\n direction.stopIds = directionStops.map((stop) => stop?.stop_id)\n\n const trips = getTrips(\n {\n route_id: route.route_id,\n direction_id: direction.direction_id,\n service_id: calendars.map(\n (calendar: Record<string, string>) => calendar.service_id,\n ),\n },\n ['trip_id'],\n )\n direction.tripIds = trips.map((trip) => trip.trip_id)\n }\n\n route.directions = directions\n filteredRoutes.push(route)\n }\n\n // Sort routes twice to handle integers with alphabetical characters, such as ['14', '14L', '14X']\n const sortedRoutes = sortBy(\n sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),\n (route) => Number.parseInt(route.route_short_name, 10),\n )\n\n // Get Parent Station Stops\n const parentStationIds = new Set(stops.map((stop) => stop?.parent_station))\n\n const parentStationStops = getStops(\n { stop_id: Array.from(parentStationIds) },\n ['stop_id', 'stop_name', 'stop_code', 'parent_station'],\n )\n\n stops.push(\n ...parentStationStops.map((stop) => {\n stop.is_parent_station = true\n return stop\n }),\n )\n\n // Sort unique list of stops\n const sortedStops = sortBy(uniqBy(stops, 'stop_id'), 'stop_name')\n\n return {\n routes: removeNulls(sortedRoutes),\n stops: removeNulls(sortedStops),\n }\n}\n\n/*\n * Remove null values from array or object\n */\nfunction removeNulls(data: any): any {\n if (Array.isArray(data)) {\n return data\n .map(removeNulls)\n .filter((item) => item !== null && item !== undefined)\n } else if (typeof data === 'object' && data !== null) {\n return Object.entries(data).reduce((acc, [key, value]) => {\n const cleanedValue = removeNulls(value)\n if (cleanedValue !== null && cleanedValue !== undefined) {\n acc[key] = cleanedValue\n }\n return acc\n }, {})\n } else {\n return data\n }\n}\n\n/*\n * Initialize configuration with defaults.\n */\nexport function setDefaultConfig(initialConfig: IConfig) {\n const defaults = {\n beautify: false,\n noHead: false,\n refreshIntervalSeconds: 20,\n skipImport: false,\n timeFormat: '12hour',\n includeCoordinates: false,\n }\n\n return Object.assign(defaults, initialConfig)\n}\n\nexport function formatWhereClause(\n key: string,\n value: null | SqlValue | SqlValue[],\n) {\n if (Array.isArray(value)) {\n let whereClause = `${sqlString.escapeId(key)} IN (${value\n .filter((v) => v !== null)\n .map((v) => sqlString.escape(v))\n .join(', ')})`\n\n if (value.includes(null)) {\n whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`\n }\n\n return whereClause\n }\n\n if (value === null) {\n return `${sqlString.escapeId(key)} IS NULL`\n }\n\n return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`\n}\n\nexport function formatWhereClauses(query: SqlWhere) {\n if (Object.keys(query).length === 0) {\n return ''\n }\n\n const whereClauses = Object.entries(query).map(([key, value]) =>\n formatWhereClause(key, value),\n )\n return `WHERE ${whereClauses.join(' AND ')}`\n}\n"],"mappings":"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","names":["path","openDb","fileURLToPath","stoptimes","fileURLToPath","openDb","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/transit-departures-widget.ts","../src/lib/file-utils.ts","../src/lib/log-utils.ts","../src/lib/utils.ts"],"sourcesContent":["import path from 'path'\nimport { clone, omit } from 'lodash-es'\nimport { writeFile } from 'node:fs/promises'\nimport { importGtfs, openDb } from 'gtfs'\nimport sanitize from 'sanitize-filename'\nimport Timer from 'timer-machine'\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"]}
|
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
|
+
"version": "2.4.4",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"transit",
|
|
7
7
|
"gtfs",
|
|
@@ -36,9 +36,8 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"gtfs": "^4.12.0",
|
|
39
|
+
"express": "^4.21.1",
|
|
40
|
+
"gtfs": "^4.15.9",
|
|
42
41
|
"i18n": "^0.15.1",
|
|
43
42
|
"js-beautify": "^1.15.1",
|
|
44
43
|
"lodash-es": "^4.17.21",
|
|
@@ -59,16 +58,16 @@
|
|
|
59
58
|
"@types/js-beautify": "^1.14.3",
|
|
60
59
|
"@types/lodash-es": "^4.17.12",
|
|
61
60
|
"@types/morgan": "^1.9.9",
|
|
62
|
-
"@types/node": "^20.
|
|
61
|
+
"@types/node": "^20.17.6",
|
|
63
62
|
"@types/pug": "^2.0.10",
|
|
64
63
|
"@types/timer-machine": "^1.1.3",
|
|
65
64
|
"@types/toposort": "^2.0.7",
|
|
66
|
-
"@types/yargs": "^17.0.
|
|
67
|
-
"husky": "^9.
|
|
68
|
-
"lint-staged": "^15.2.
|
|
69
|
-
"prettier": "^3.3.
|
|
70
|
-
"tsup": "^8.
|
|
71
|
-
"typescript": "^5.
|
|
65
|
+
"@types/yargs": "^17.0.33",
|
|
66
|
+
"husky": "^9.1.6",
|
|
67
|
+
"lint-staged": "^15.2.10",
|
|
68
|
+
"prettier": "^3.3.3",
|
|
69
|
+
"tsup": "^8.3.5",
|
|
70
|
+
"typescript": "^5.6.3"
|
|
72
71
|
},
|
|
73
72
|
"engines": {
|
|
74
73
|
"node": ">= 14.15.4"
|
|
@@ -81,6 +80,9 @@
|
|
|
81
80
|
"@release-it/keep-a-changelog": {
|
|
82
81
|
"filename": "CHANGELOG.md"
|
|
83
82
|
}
|
|
83
|
+
},
|
|
84
|
+
"hooks": {
|
|
85
|
+
"after:bump": "npm run build"
|
|
84
86
|
}
|
|
85
87
|
},
|
|
86
88
|
"prettier": {
|