transit-departures-widget 2.3.0 → 2.4.1

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.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,331 @@
1
+ // src/app/index.ts
2
+ import path2 from "node:path";
3
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
4
+ import { readFileSync } from "node:fs";
5
+ import yargs from "yargs";
6
+ import { openDb as openDb2 } from "gtfs";
7
+ import express, { Router } from "express";
8
+ import logger from "morgan";
9
+
10
+ // src/lib/utils.ts
11
+ import { fileURLToPath as fileURLToPath2 } from "url";
12
+ import { dirname, join } from "path";
13
+ import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
14
+ import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
15
+
16
+ // src/lib/file-utils.ts
17
+ import path from "path";
18
+ import { fileURLToPath } from "node:url";
19
+ import copydir from "copy-dir";
20
+ import beautify from "js-beautify";
21
+ import pug from "pug";
22
+ import untildify from "untildify";
23
+ function getTemplatePath(templateFileName, config2) {
24
+ let fullTemplateFileName = templateFileName;
25
+ if (config2.noHead !== true) {
26
+ fullTemplateFileName += "_full";
27
+ }
28
+ if (config2.templatePath !== void 0) {
29
+ return path.join(
30
+ untildify(config2.templatePath),
31
+ `${fullTemplateFileName}.pug`
32
+ );
33
+ }
34
+ return path.join(
35
+ fileURLToPath(import.meta.url),
36
+ "../../../views/widget",
37
+ `${fullTemplateFileName}.pug`
38
+ );
39
+ }
40
+ async function renderFile(templateFileName, templateVars, config2) {
41
+ const templatePath = getTemplatePath(templateFileName, config2);
42
+ const html = await pug.renderFile(templatePath, templateVars);
43
+ if (config2.beautify === true) {
44
+ return beautify.html_beautify(html, { indent_size: 2 });
45
+ }
46
+ return html;
47
+ }
48
+
49
+ // src/lib/utils.ts
50
+ import sqlString from "sqlstring-sqlite";
51
+ import toposort from "toposort";
52
+ import i18n from "i18n";
53
+ var getCalendarsForDateRange = (config2) => {
54
+ const db = openDb(config2);
55
+ let whereClause = "";
56
+ const whereClauses = [];
57
+ if (config2.endDate) {
58
+ whereClauses.push(`start_date <= ${sqlString.escape(config2.endDate)}`);
59
+ }
60
+ if (config2.startDate) {
61
+ whereClauses.push(`end_date >= ${sqlString.escape(config2.startDate)}`);
62
+ }
63
+ if (whereClauses.length > 0) {
64
+ whereClause = `WHERE ${whereClauses.join(" AND ")}`;
65
+ }
66
+ return db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
67
+ };
68
+ function formatRouteName(route) {
69
+ let routeName = "";
70
+ if (route.route_short_name !== null) {
71
+ routeName += route.route_short_name;
72
+ }
73
+ if (route.route_short_name !== null && route.route_long_name !== null) {
74
+ routeName += " - ";
75
+ }
76
+ if (route.route_long_name !== null) {
77
+ routeName += route.route_long_name;
78
+ }
79
+ return routeName;
80
+ }
81
+ function getDirectionsForRoute(route, config2) {
82
+ const db = openDb(config2);
83
+ const directions = getDirections({ route_id: route.route_id }, [
84
+ "direction_id",
85
+ "direction"
86
+ ]);
87
+ const calendars = getCalendarsForDateRange(config2);
88
+ if (directions.length === 0) {
89
+ const headsigns = db.prepare(
90
+ `SELECT direction_id, trip_headsign, count(*) AS count FROM trips WHERE route_id = ? AND service_id IN (${calendars.map((calendar) => `'${calendar.service_id}'`).join(", ")}) GROUP BY direction_id, trip_headsign`
91
+ ).all(route.route_id);
92
+ for (const group of Object.values(groupBy(headsigns, "direction_id"))) {
93
+ const mostCommonHeadsign = maxBy(group, "count");
94
+ directions.push({
95
+ direction_id: mostCommonHeadsign.direction_id,
96
+ direction: i18n.__("To {{{headsign}}}", {
97
+ headsign: mostCommonHeadsign.trip_headsign
98
+ })
99
+ });
100
+ }
101
+ }
102
+ return directions;
103
+ }
104
+ function sortStopIdsBySequence(stoptimes) {
105
+ const stoptimesGroupedByTrip = groupBy(stoptimes, "trip_id");
106
+ try {
107
+ const stopGraph = [];
108
+ for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {
109
+ const sortedStopIds = sortBy(tripStoptimes, "stop_sequence").map(
110
+ (stoptime) => stoptime.stop_id
111
+ );
112
+ for (const [index, stopId] of sortedStopIds.entries()) {
113
+ if (index === sortedStopIds.length - 1) {
114
+ continue;
115
+ }
116
+ stopGraph.push([stopId, sortedStopIds[index + 1]]);
117
+ }
118
+ }
119
+ return toposort(stopGraph);
120
+ } catch {
121
+ }
122
+ const longestTripStoptimes = maxBy(
123
+ Object.values(stoptimesGroupedByTrip),
124
+ (stoptimes2) => size(stoptimes2)
125
+ );
126
+ return longestTripStoptimes.map((stoptime) => stoptime.stop_id);
127
+ }
128
+ function getStopsForDirection(route, direction, config2) {
129
+ const db = openDb(config2);
130
+ const calendars = getCalendarsForDateRange(config2);
131
+ const whereClause = formatWhereClauses({
132
+ direction_id: direction.direction_id,
133
+ route_id: route.route_id,
134
+ service_id: calendars.map((calendar) => calendar.service_id)
135
+ });
136
+ const stoptimes = db.prepare(
137
+ `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`
138
+ ).all();
139
+ const sortedStopIds = sortStopIdsBySequence(stoptimes);
140
+ const deduplicatedStopIds = sortedStopIds.reduce((memo, stopId) => {
141
+ if (last(memo) !== stopId) {
142
+ memo.push(stopId);
143
+ }
144
+ return memo;
145
+ }, []);
146
+ deduplicatedStopIds.pop();
147
+ const stops = getStops({ stop_id: deduplicatedStopIds }, [
148
+ "stop_id",
149
+ "stop_name",
150
+ "stop_code",
151
+ "parent_station"
152
+ ]);
153
+ return deduplicatedStopIds.map(
154
+ (stopId) => stops.find((stop) => stop.stop_id === stopId)
155
+ );
156
+ }
157
+ function generateTransitDeparturesWidgetHtml(config2) {
158
+ i18n.configure({
159
+ directory: join(dirname(fileURLToPath2(import.meta.url)), "../../locales"),
160
+ defaultLocale: config2.locale,
161
+ updateFiles: false
162
+ });
163
+ const templateVars = {
164
+ __: i18n.__,
165
+ config: config2
166
+ };
167
+ return renderFile("widget", templateVars, config2);
168
+ }
169
+ function generateTransitDeparturesWidgetJson(config2) {
170
+ const routes = getRoutes();
171
+ const stops = [];
172
+ const filteredRoutes = [];
173
+ const calendars = getCalendarsForDateRange(config2);
174
+ for (const route of routes) {
175
+ route.route_full_name = formatRouteName(route);
176
+ const directions = getDirectionsForRoute(route, config2);
177
+ if (directions.length === 0) {
178
+ config2.logWarning(
179
+ `route_id ${route.route_id} has no directions - skipping`
180
+ );
181
+ continue;
182
+ }
183
+ for (const direction of directions) {
184
+ const directionStops = getStopsForDirection(route, direction, config2);
185
+ stops.push(...directionStops);
186
+ direction.stopIds = directionStops.map((stop) => stop?.stop_id);
187
+ const trips = getTrips(
188
+ {
189
+ route_id: route.route_id,
190
+ direction_id: direction.direction_id,
191
+ service_id: calendars.map((calendar) => calendar.service_id)
192
+ },
193
+ ["trip_id"]
194
+ );
195
+ direction.tripIds = trips.map((trip) => trip.trip_id);
196
+ }
197
+ route.directions = directions;
198
+ filteredRoutes.push(route);
199
+ }
200
+ const sortedRoutes = sortBy(
201
+ sortBy(filteredRoutes, (route) => route.route_short_name?.toLowerCase()),
202
+ (route) => Number.parseInt(route.route_short_name, 10)
203
+ );
204
+ const parentStationIds = new Set(stops.map((stop) => stop?.parent_station));
205
+ const parentStationStops = getStops(
206
+ { stop_id: Array.from(parentStationIds) },
207
+ ["stop_id", "stop_name", "stop_code", "parent_station"]
208
+ );
209
+ stops.push(
210
+ ...parentStationStops.map((stop) => {
211
+ stop.is_parent_station = true;
212
+ return stop;
213
+ })
214
+ );
215
+ const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
216
+ return {
217
+ routes: removeNulls(sortedRoutes),
218
+ stops: removeNulls(sortedStops)
219
+ };
220
+ }
221
+ function removeNulls(data) {
222
+ if (Array.isArray(data)) {
223
+ return data.map(removeNulls).filter((item) => item !== null && item !== void 0);
224
+ } else if (typeof data === "object" && data !== null) {
225
+ return Object.entries(data).reduce((acc, [key, value]) => {
226
+ const cleanedValue = removeNulls(value);
227
+ if (cleanedValue !== null && cleanedValue !== void 0) {
228
+ acc[key] = cleanedValue;
229
+ }
230
+ return acc;
231
+ }, {});
232
+ } else {
233
+ return data;
234
+ }
235
+ }
236
+ function setDefaultConfig(initialConfig) {
237
+ const defaults = {
238
+ beautify: false,
239
+ noHead: false,
240
+ refreshIntervalSeconds: 20,
241
+ skipImport: false,
242
+ timeFormat: "12hour"
243
+ };
244
+ return Object.assign(defaults, initialConfig);
245
+ }
246
+ function formatWhereClause(key, value) {
247
+ if (Array.isArray(value)) {
248
+ let whereClause = `${sqlString.escapeId(key)} IN (${value.filter((v) => v !== null).map((v) => sqlString.escape(v)).join(", ")})`;
249
+ if (value.includes(null)) {
250
+ whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`;
251
+ }
252
+ return whereClause;
253
+ }
254
+ if (value === null) {
255
+ return `${sqlString.escapeId(key)} IS NULL`;
256
+ }
257
+ return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`;
258
+ }
259
+ function formatWhereClauses(query) {
260
+ if (Object.keys(query).length === 0) {
261
+ return "";
262
+ }
263
+ const whereClauses = Object.entries(query).map(
264
+ ([key, value]) => formatWhereClause(key, value)
265
+ );
266
+ return `WHERE ${whereClauses.join(" AND ")}`;
267
+ }
268
+
269
+ // src/app/index.ts
270
+ var argv = yargs(process.argv).option("c", {
271
+ alias: "configPath",
272
+ describe: "Path to config file",
273
+ default: "./config.json",
274
+ type: "string"
275
+ }).parseSync();
276
+ var app = express();
277
+ var router = Router();
278
+ var configPath = argv.configPath || new URL("../../config.json", import.meta.url);
279
+ var selectedConfig = JSON.parse(readFileSync(configPath).toString());
280
+ var config = setDefaultConfig(selectedConfig);
281
+ config.noHead = false;
282
+ config.assetPath = "/";
283
+ config.log = console.log;
284
+ config.logWarning = console.warn;
285
+ config.logError = console.error;
286
+ try {
287
+ openDb2(config);
288
+ } catch (error) {
289
+ if (error?.code === "SQLITE_CANTOPEN") {
290
+ config.logError(
291
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
292
+ );
293
+ }
294
+ throw error;
295
+ }
296
+ router.get("/", async (request, response, next) => {
297
+ try {
298
+ const html = await generateTransitDeparturesWidgetHtml(config);
299
+ response.send(html);
300
+ } catch (error) {
301
+ next(error);
302
+ }
303
+ });
304
+ router.get("/data/routes.json", async (request, response, next) => {
305
+ try {
306
+ const { routes } = await generateTransitDeparturesWidgetJson(config);
307
+ response.json(routes);
308
+ } catch (error) {
309
+ next(error);
310
+ }
311
+ });
312
+ router.get("/data/stops.json", async (request, response, next) => {
313
+ try {
314
+ const { stops } = await generateTransitDeparturesWidgetJson(config);
315
+ response.json(stops);
316
+ } catch (error) {
317
+ next(error);
318
+ }
319
+ });
320
+ app.set("views", path2.join(fileURLToPath3(import.meta.url), "../../../views"));
321
+ app.set("view engine", "pug");
322
+ app.use(logger("dev"));
323
+ app.use(
324
+ express.static(path2.join(fileURLToPath3(import.meta.url), "../../../public"))
325
+ );
326
+ app.use("/", router);
327
+ app.set("port", process.env.PORT || 3e3);
328
+ var server = app.listen(app.get("port"), () => {
329
+ console.log(`Express server listening on port ${app.get("port")}`);
330
+ });
331
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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).option('c', {\n alias: 'configPath',\n describe: 'Path to config file',\n default: './config.json',\n type: 'string',\n}).parseSync()\n\nconst app = express()\nconst router = Router()\n\nconst configPath = (argv.configPath || 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(stopGraph as unknown as readonly [string, string | undefined][])\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((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 // Remove last stop of route since boarding is not allowed\n deduplicatedStopIds.pop()\n\n // Fetch stop details\n const stops = getStops({ stop_id: deduplicatedStopIds }, [\n 'stop_id',\n 'stop_name',\n 'stop_code',\n 'parent_station',\n ])\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((calendar: Record<string, string>) => calendar.service_id),\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 }\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(templateFileName: string, templateVars : any, config: IConfig) {\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":";AAAA,OAAOA,WAAU;AACjB,SAAS,iBAAAC,sBAAqB;AAC9B,SAAS,oBAAoB;AAC7B,OAAO,WAAW;AAClB,SAAS,UAAAC,eAAc;AAEvB,OAAO,WAAW,cAAc;AAChC,OAAO,YAAY;;;ACPnB,SAAS,iBAAAC,sBAAqB;AAC9B,SAAS,SAAS,YAAY;AAC9B,SAAS,QAAQ,eAAe,WAAW,UAAU,gBAAgB;AACrE,SAAS,SAAS,MAAM,OAAO,MAAM,QAAQ,cAAc;;;ACH3D,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,OAAO,aAAa;AACpB,OAAO,cAAc;AACrB,OAAO,SAAS;AAChB,OAAO,eAAe;AAyCtB,SAAS,gBAAgB,kBAAkBC,SAAQ;AACjD,MAAI,uBAAuB;AAC3B,MAAIA,QAAO,WAAW,MAAM;AAC1B,4BAAwB;AAAA,EAC1B;AAEA,MAAIA,QAAO,iBAAiB,QAAW;AACrC,WAAO,KAAK;AAAA,MACV,UAAUA,QAAO,YAAY;AAAA,MAC7B,GAAG,oBAAoB;AAAA,IACzB;AAAA,EACF;AAEA,SAAO,KAAK;AAAA,IACV,cAAc,YAAY,GAAG;AAAA,IAC7B;AAAA,IACA,GAAG,oBAAoB;AAAA,EACzB;AACF;AAoCA,eAAsB,WAAW,kBAA0B,cAAoBC,SAAiB;AAC9F,QAAM,eAAe,gBAAgB,kBAAkBA,OAAM;AAC7D,QAAM,OAAO,MAAM,IAAI,WAAW,cAAc,YAAY;AAG5D,MAAIA,QAAO,aAAa,MAAM;AAC5B,WAAO,SAAS,cAAc,MAAM,EAAE,aAAa,EAAE,CAAC;AAAA,EACxD;AAEA,SAAO;AACT;;;AD1GA,OAAO,eAAe;AACtB,OAAO,cAAc;AACrB,OAAO,UAAU;AAOjB,IAAM,2BAA2B,CAACC,YAAoB;AACpD,QAAM,KAAK,OAAOA,OAAM;AACxB,MAAI,cAAc;AAClB,QAAM,eAAe,CAAC;AAEtB,MAAIA,QAAO,SAAS;AAClB,iBAAa,KAAK,iBAAiB,UAAU,OAAOA,QAAO,OAAO,CAAC,EAAE;AAAA,EACvE;AAEA,MAAIA,QAAO,WAAW;AACpB,iBAAa,KAAK,eAAe,UAAU,OAAOA,QAAO,SAAS,CAAC,EAAE;AAAA,EACvE;AAEA,MAAI,aAAa,SAAS,GAAG;AAC3B,kBAAc,SAAS,aAAa,KAAK,OAAO,CAAC;AAAA,EACnD;AAEA,SAAO,GAAG,QAAQ,0BAA0B,WAAW,EAAE,EAAE,IAAI;AACjE;AAKA,SAAS,gBAAgB,OAAO;AAC9B,MAAI,YAAY;AAEhB,MAAI,MAAM,qBAAqB,MAAM;AACnC,iBAAa,MAAM;AAAA,EACrB;AAEA,MAAI,MAAM,qBAAqB,QAAQ,MAAM,oBAAoB,MAAM;AACrE,iBAAa;AAAA,EACf;AAEA,MAAI,MAAM,oBAAoB,MAAM;AAClC,iBAAa,MAAM;AAAA,EACrB;AAEA,SAAO;AACT;AAKA,SAAS,sBAAsB,OAA+BA,SAAkB;AAC9E,QAAM,KAAK,OAAOA,OAAM;AAGxB,QAAM,aAAa,cAAc,EAAE,UAAU,MAAM,SAAS,GAAG;AAAA,IAC7D;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,YAAY,yBAAyBA,OAAM;AAGjD,MAAI,WAAW,WAAW,GAAG;AAC3B,UAAM,YAAY,GACf;AAAA,MACC,0GAA0G,UACvG,IAAI,CAAC,aAAqC,IAAI,SAAS,UAAU,GAAG,EACpE,KAAK,IAAI,CAAC;AAAA,IACf,EACC,IAAI,MAAM,QAAQ;AAErB,eAAW,SAAS,OAAO,OAAO,QAAQ,WAAW,cAAc,CAAC,GAAG;AACrE,YAAM,qBAAqB,MAAM,OAAO,OAAO;AAC/C,iBAAW,KAAK;AAAA,QACd,cAAc,mBAAmB;AAAA,QACjC,WAAW,KAAK,GAAG,qBAAqB;AAAA,UACtC,UAAU,mBAAmB;AAAA,QAC/B,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAKA,SAAS,sBAAsB,WAAqC;AAClE,QAAM,yBAAyB,QAAQ,WAAW,SAAS;AAG3D,MAAI;AACF,UAAM,YAAY,CAAC;AAEnB,eAAW,iBAAiB,OAAO,OAAO,sBAAsB,GAAG;AACjE,YAAM,gBAAgB,OAAO,eAAe,eAAe,EAAE;AAAA,QAC3D,CAAC,aAAa,SAAS;AAAA,MACzB;AAEA,iBAAW,CAAC,OAAO,MAAM,KAAK,cAAc,QAAQ,GAAG;AACrD,YAAI,UAAU,cAAc,SAAS,GAAG;AACtC;AAAA,QACF;AAEA,kBAAU,KAAK,CAAC,QAAQ,cAAc,QAAQ,CAAC,CAAC,CAAC;AAAA,MACnD;AAAA,IACF;AAEA,WAAO,SAAS,SAA+D;AAAA,EACjF,QAAQ;AAAA,EAER;AAGA,QAAM,uBAAuB;AAAA,IAC3B,OAAO,OAAO,sBAAsB;AAAA,IACpC,CAACC,eAAc,KAAKA,UAAS;AAAA,EAC/B;AAEA,SAAO,qBAAqB,IAAI,CAAC,aAAa,SAAS,OAAO;AAChE;AAKA,SAAS,qBAAqB,OAAO,WAAWD,SAAiB;AAC/D,QAAM,KAAK,OAAOA,OAAM;AACxB,QAAM,YAAY,yBAAyBA,OAAM;AACjD,QAAM,cAAc,mBAAmB;AAAA,IACrC,cAAc,UAAU;AAAA,IACxB,UAAU,MAAM;AAAA,IAChB,YAAY,UAAU,IAAI,CAAC,aAAa,SAAS,UAAU;AAAA,EAC7D,CAAC;AACD,QAAM,YAAY,GACf;AAAA,IACC,sGAAsG,WAAW;AAAA,EACnH,EACC,IAAI;AAEP,QAAM,gBAAgB,sBAAsB,SAAS;AAErD,QAAM,sBAAsB,cAAc,OAAO,CAAC,MAAiB,WAAmB;AAEpF,QAAI,KAAK,IAAI,MAAM,QAAQ;AACzB,WAAK,KAAK,MAAM;AAAA,IAClB;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAGL,sBAAoB,IAAI;AAGxB,QAAM,QAAQ,SAAS,EAAE,SAAS,oBAAoB,GAAG;AAAA,IACvD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO,oBAAoB;AAAA,IAAI,CAAC,WAC9B,MAAM,KAAK,CAAC,SAAS,KAAK,YAAY,MAAM;AAAA,EAC9C;AACF;AAKO,SAAS,oCAAoCA,SAAiB;AACnE,OAAK,UAAU;AAAA,IACb,WAAW,KAAK,QAAQE,eAAc,YAAY,GAAG,CAAC,GAAG,eAAe;AAAA,IACxE,eAAeF,QAAO;AAAA,IACtB,aAAa;AAAA,EACf,CAAC;AAED,QAAM,eAAe;AAAA,IACnB,IAAI,KAAK;AAAA,IACT,QAAAA;AAAA,EACF;AACA,SAAO,WAAW,UAAU,cAAcA,OAAM;AAClD;AAKO,SAAS,oCAAoCA,SAAiB;AACnE,QAAM,SAAS,UAAU;AACzB,QAAM,QAAQ,CAAC;AACf,QAAM,iBAAiB,CAAC;AACxB,QAAM,YAAY,yBAAyBA,OAAM;AAEjD,aAAW,SAAS,QAAQ;AAC1B,UAAM,kBAAkB,gBAAgB,KAAK;AAE7C,UAAM,aAAa,sBAAsB,OAAOA,OAAM;AAGtD,QAAI,WAAW,WAAW,GAAG;AAC3B,MAAAA,QAAO;AAAA,QACL,YAAY,MAAM,QAAQ;AAAA,MAC5B;AACA;AAAA,IACF;AAEA,eAAW,aAAa,YAAY;AAClC,YAAM,iBAAiB,qBAAqB,OAAO,WAAWA,OAAM;AACpE,YAAM,KAAK,GAAG,cAAc;AAC5B,gBAAU,UAAU,eAAe,IAAI,CAAC,SAAS,MAAM,OAAO;AAE9D,YAAM,QAAQ;AAAA,QACZ;AAAA,UACE,UAAU,MAAM;AAAA,UAChB,cAAc,UAAU;AAAA,UACxB,YAAY,UAAU,IAAI,CAAC,aAAqC,SAAS,UAAU;AAAA,QACrF;AAAA,QACA,CAAC,SAAS;AAAA,MACZ;AACA,gBAAU,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO;AAAA,IACtD;AAEA,UAAM,aAAa;AACnB,mBAAe,KAAK,KAAK;AAAA,EAC3B;AAGA,QAAM,eAAe;AAAA,IACnB,OAAO,gBAAgB,CAAC,UAAU,MAAM,kBAAkB,YAAY,CAAC;AAAA,IACvE,CAAC,UAAU,OAAO,SAAS,MAAM,kBAAkB,EAAE;AAAA,EACvD;AAGA,QAAM,mBAAmB,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,MAAM,cAAc,CAAC;AAE1E,QAAM,qBAAqB;AAAA,IACzB,EAAE,SAAS,MAAM,KAAK,gBAAgB,EAAE;AAAA,IACxC,CAAC,WAAW,aAAa,aAAa,gBAAgB;AAAA,EACxD;AAEA,QAAM;AAAA,IACJ,GAAG,mBAAmB,IAAI,CAAC,SAAS;AAClC,WAAK,oBAAoB;AACzB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAGA,QAAM,cAAc,OAAO,OAAO,OAAO,SAAS,GAAG,WAAW;AAEhE,SAAO;AAAA,IACL,QAAQ,YAAY,YAAY;AAAA,IAChC,OAAO,YAAY,WAAW;AAAA,EAChC;AACF;AAKA,SAAS,YAAY,MAAgB;AACnC,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,KACJ,IAAI,WAAW,EACf,OAAO,CAAC,SAAS,SAAS,QAAQ,SAAS,MAAS;AAAA,EACzD,WAAW,OAAO,SAAS,YAAY,SAAS,MAAM;AACpD,WAAO,OAAO,QAAQ,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM;AACxD,YAAM,eAAe,YAAY,KAAK;AACtC,UAAI,iBAAiB,QAAQ,iBAAiB,QAAW;AACvD,YAAI,GAAG,IAAI;AAAA,MACb;AACA,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACP,OAAO;AACL,WAAO;AAAA,EACT;AACF;AAKO,SAAS,iBAAiB,eAAwB;AACvD,QAAM,WAAW;AAAA,IACf,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,wBAAwB;AAAA,IACxB,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAEA,SAAO,OAAO,OAAO,UAAU,aAAa;AAC9C;AAEO,SAAS,kBACd,KACA,OACA;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,QAAI,cAAc,GAAG,UAAU,SAAS,GAAG,CAAC,QAAQ,MACjD,OAAO,CAAC,MAAM,MAAM,IAAI,EACxB,IAAI,CAAC,MAAM,UAAU,OAAO,CAAC,CAAC,EAC9B,KAAK,IAAI,CAAC;AAEb,QAAI,MAAM,SAAS,IAAI,GAAG;AACxB,oBAAc,IAAI,WAAW,OAAO,UAAU,SAAS,GAAG,CAAC;AAAA,IAC7D;AAEA,WAAO;AAAA,EACT;AAEA,MAAI,UAAU,MAAM;AAClB,WAAO,GAAG,UAAU,SAAS,GAAG,CAAC;AAAA,EACnC;AAEA,SAAO,GAAG,UAAU,SAAS,GAAG,CAAC,MAAM,UAAU,OAAO,KAAK,CAAC;AAChE;AAEO,SAAS,mBAAmB,OAAiB;AAClD,MAAI,OAAO,KAAK,KAAK,EAAE,WAAW,GAAG;AACnC,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,OAAO,QAAQ,KAAK,EAAE;AAAA,IAAI,CAAC,CAAC,KAAK,KAAK,MACzD,kBAAkB,KAAK,KAAK;AAAA,EAC9B;AACA,SAAO,SAAS,aAAa,KAAK,OAAO,CAAC;AAC5C;;;AD9TA,IAAM,OAAO,MAAM,QAAQ,IAAI,EAAE,OAAO,KAAK;AAAA,EAC3C,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AACR,CAAC,EAAE,UAAU;AAEb,IAAM,MAAM,QAAQ;AACpB,IAAM,SAAS,OAAO;AAEtB,IAAM,aAAc,KAAK,cAAc,IAAI,IAAI,qBAAqB,YAAY,GAAG;AAEnF,IAAM,iBAAiB,KAAK,MAAM,aAAa,UAAU,EAAE,SAAS,CAAC;AAErE,IAAM,SAAS,iBAAiB,cAAc;AAE9C,OAAO,SAAS;AAChB,OAAO,YAAY;AACnB,OAAO,MAAM,QAAQ;AACrB,OAAO,aAAa,QAAQ;AAC5B,OAAO,WAAW,QAAQ;AAE1B,IAAI;AACF,EAAAG,QAAO,MAAM;AACf,SAAS,OAAY;AACnB,MAAI,OAAO,SAAS,mBAAmB;AACrC,WAAO;AAAA,MACL,mCAAmC,OAAO,UAAU;AAAA,IACtD;AAAA,EACF;AAEA,QAAM;AACR;AAKA,OAAO,IAAI,KAAK,OAAO,SAAS,UAAU,SAAS;AACjD,MAAI;AACF,UAAM,OAAO,MAAM,oCAAoC,MAAM;AAC7D,aAAS,KAAK,IAAI;AAAA,EACpB,SAAS,OAAO;AACd,SAAK,KAAK;AAAA,EACZ;AACF,CAAC;AAKD,OAAO,IAAI,qBAAqB,OAAO,SAAS,UAAU,SAAS;AACjE,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,oCAAoC,MAAM;AACnE,aAAS,KAAK,MAAM;AAAA,EACtB,SAAS,OAAO;AACd,SAAK,KAAK;AAAA,EACZ;AACF,CAAC;AAED,OAAO,IAAI,oBAAoB,OAAO,SAAS,UAAU,SAAS;AAChE,MAAI;AACF,UAAM,EAAE,MAAM,IAAI,MAAM,oCAAoC,MAAM;AAClE,aAAS,KAAK,KAAK;AAAA,EACrB,SAAS,OAAO;AACd,SAAK,KAAK;AAAA,EACZ;AACF,CAAC;AAED,IAAI,IAAI,SAASC,MAAK,KAAKC,eAAc,YAAY,GAAG,GAAG,gBAAgB,CAAC;AAC5E,IAAI,IAAI,eAAe,KAAK;AAE5B,IAAI,IAAI,OAAO,KAAK,CAAC;AACrB,IAAI;AAAA,EACF,QAAQ,OAAOD,MAAK,KAAKC,eAAc,YAAY,GAAG,GAAG,iBAAiB,CAAC;AAC7E;AAEA,IAAI,IAAI,KAAK,MAAM;AACnB,IAAI,IAAI,QAAQ,QAAQ,IAAI,QAAQ,GAAI;AAExC,IAAM,SAAS,IAAI,OAAO,IAAI,IAAI,MAAM,GAAG,MAAM;AAC/C,UAAQ,IAAI,oCAAoC,IAAI,IAAI,MAAM,CAAC,EAAE;AACnE,CAAC;","names":["path","fileURLToPath","openDb","fileURLToPath","config","config","config","stoptimes","fileURLToPath","openDb","path","fileURLToPath"]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node