transit-departures-widget 2.7.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/app/index.d.ts +1 -2
- package/dist/app/index.js +81 -552
- package/dist/app/index.js.map +1 -1
- package/dist/bin/transit-departures-widget.d.ts +1 -1
- package/dist/bin/transit-departures-widget.js +23 -638
- package/dist/bin/transit-departures-widget.js.map +1 -1
- package/dist/browser/THIRD_PARTY_LICENSES.txt +87 -0
- package/dist/browser/pbf.js +1 -0
- package/dist/index.d.ts +29 -26
- package/dist/index.js +2 -590
- package/dist/src-D_ggoL9P.js +55 -0
- package/dist/src-D_ggoL9P.js.map +1 -0
- package/dist/utils-BxiS6d7j.js +368 -0
- package/dist/utils-BxiS6d7j.js.map +1 -0
- package/package.json +11 -12
- package/dist/frontend_libraries/pbf.js +0 -1
- package/dist/index.js.map +0 -1
- /package/dist/{frontend_libraries → browser}/accessible-autocomplete.min.css +0 -0
- /package/dist/{frontend_libraries → browser}/accessible-autocomplete.min.js +0 -0
- /package/dist/{frontend_libraries → browser}/gtfs-realtime.browser.proto.js +0 -0
|
@@ -1,649 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { a as formatError, s as getConfig } from "../utils-BxiS6d7j.js";
|
|
3
|
+
import { t as transitDeparturesWidget } from "../src-D_ggoL9P.js";
|
|
4
4
|
import yargs from "yargs";
|
|
5
5
|
import { hideBin } from "yargs/helpers";
|
|
6
6
|
import PrettyError from "pretty-error";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mkdir,
|
|
16
|
-
readdir,
|
|
17
|
-
readFile,
|
|
18
|
-
rm
|
|
19
|
-
} from "fs/promises";
|
|
20
|
-
import beautify from "js-beautify";
|
|
21
|
-
import pug from "pug";
|
|
22
|
-
import untildify from "untildify";
|
|
23
|
-
async function getConfig(argv2) {
|
|
24
|
-
try {
|
|
25
|
-
const data = await readFile(
|
|
26
|
-
resolve(untildify(argv2.configPath)),
|
|
27
|
-
"utf8"
|
|
28
|
-
).catch((error) => {
|
|
29
|
-
console.error(
|
|
30
|
-
new Error(
|
|
31
|
-
`Cannot find configuration file at \`${argv2.configPath}\`. Use config-sample.json as a starting point, pass --configPath option`
|
|
32
|
-
)
|
|
33
|
-
);
|
|
34
|
-
throw error;
|
|
35
|
-
});
|
|
36
|
-
const config = JSON.parse(data);
|
|
37
|
-
if (argv2.skipImport === true) {
|
|
38
|
-
config.skipImport = argv2.skipImport;
|
|
39
|
-
}
|
|
40
|
-
return config;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error(
|
|
43
|
-
new Error(
|
|
44
|
-
`Cannot parse configuration file at \`${argv2.configPath}\`. Check to ensure that it is valid JSON.`
|
|
45
|
-
)
|
|
46
|
-
);
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function getPathToThisModuleFolder() {
|
|
51
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
52
|
-
let distFolderPath;
|
|
53
|
-
if (__dirname.endsWith("/dist/bin") || __dirname.endsWith("/dist/app")) {
|
|
54
|
-
distFolderPath = resolve(__dirname, "../../");
|
|
55
|
-
} else if (__dirname.endsWith("/dist")) {
|
|
56
|
-
distFolderPath = resolve(__dirname, "../");
|
|
57
|
-
} else {
|
|
58
|
-
distFolderPath = resolve(__dirname, "../../");
|
|
59
|
-
}
|
|
60
|
-
return distFolderPath;
|
|
61
|
-
}
|
|
62
|
-
function getPathToViewsFolder(config) {
|
|
63
|
-
if (config.templatePath) {
|
|
64
|
-
return untildify(config.templatePath);
|
|
65
|
-
}
|
|
66
|
-
return join(getPathToThisModuleFolder(), "views/widget");
|
|
67
|
-
}
|
|
68
|
-
function getPathToTemplateFile(templateFileName, config) {
|
|
69
|
-
const fullTemplateFileName = config.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
|
|
70
|
-
return join(getPathToViewsFolder(config), fullTemplateFileName);
|
|
71
|
-
}
|
|
72
|
-
async function prepDirectory(outputPath, config) {
|
|
73
|
-
try {
|
|
74
|
-
await access(outputPath);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
try {
|
|
77
|
-
await mkdir(outputPath, { recursive: true });
|
|
78
|
-
await mkdir(join(outputPath, "data"));
|
|
79
|
-
} catch (error2) {
|
|
80
|
-
if (error2?.code === "ENOENT") {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Unable to write to ${outputPath}. Try running this command from a writable directory.`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
throw error2;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const files = await readdir(outputPath);
|
|
89
|
-
if (config.overwriteExistingFiles === false && files.length > 0) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Output directory ${outputPath} is not empty. Please specify an empty directory.`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
if (config.overwriteExistingFiles === true) {
|
|
95
|
-
await rm(join(outputPath, "*"), { recursive: true, force: true });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async function copyStaticAssets(config, outputPath) {
|
|
99
|
-
const viewsFolderPath = getPathToViewsFolder(config);
|
|
100
|
-
const thisModuleFolderPath = getPathToThisModuleFolder();
|
|
101
|
-
const foldersToCopy = ["css", "js"];
|
|
102
|
-
for (const folder of foldersToCopy) {
|
|
103
|
-
if (await access(join(viewsFolderPath, folder)).then(() => true).catch(() => false)) {
|
|
104
|
-
await cp(join(viewsFolderPath, folder), join(outputPath, folder), {
|
|
105
|
-
recursive: true
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
await copyFile(
|
|
110
|
-
join(thisModuleFolderPath, "dist/frontend_libraries/pbf.js"),
|
|
111
|
-
join(outputPath, "js/pbf.js")
|
|
112
|
-
);
|
|
113
|
-
await copyFile(
|
|
114
|
-
join(
|
|
115
|
-
thisModuleFolderPath,
|
|
116
|
-
"dist/frontend_libraries/gtfs-realtime.browser.proto.js"
|
|
117
|
-
),
|
|
118
|
-
join(outputPath, "js/gtfs-realtime.browser.proto.js")
|
|
119
|
-
);
|
|
120
|
-
await copyFile(
|
|
121
|
-
join(
|
|
122
|
-
thisModuleFolderPath,
|
|
123
|
-
"dist/frontend_libraries/accessible-autocomplete.min.js"
|
|
124
|
-
),
|
|
125
|
-
join(outputPath, "js/accessible-autocomplete.min.js")
|
|
126
|
-
);
|
|
127
|
-
await copyFile(
|
|
128
|
-
join(
|
|
129
|
-
thisModuleFolderPath,
|
|
130
|
-
"dist/frontend_libraries/accessible-autocomplete.min.css"
|
|
131
|
-
),
|
|
132
|
-
join(outputPath, "css/accessible-autocomplete.min.css")
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
async function renderFile(templateFileName, templateVars, config) {
|
|
136
|
-
const templatePath = getPathToTemplateFile(templateFileName, config);
|
|
137
|
-
const html = await pug.renderFile(templatePath, templateVars);
|
|
138
|
-
if (config.beautify === true) {
|
|
139
|
-
return beautify.html_beautify(html, { indent_size: 2 });
|
|
140
|
-
}
|
|
141
|
-
return html;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// src/lib/logging/log.ts
|
|
145
|
-
import { clearLine, cursorTo } from "readline";
|
|
146
|
-
import { noop } from "lodash-es";
|
|
147
|
-
import * as colors from "yoctocolors";
|
|
148
|
-
var formatWarning = (text) => {
|
|
149
|
-
const warningMessage = `${colors.underline("Warning")}: ${text}`;
|
|
150
|
-
return colors.yellow(warningMessage);
|
|
151
|
-
};
|
|
152
|
-
var formatError = (error) => {
|
|
153
|
-
const messageText = error instanceof Error ? error.message : error;
|
|
154
|
-
const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
|
|
155
|
-
"Error: ",
|
|
156
|
-
""
|
|
157
|
-
)}`;
|
|
158
|
-
return colors.red(errorMessage);
|
|
159
|
-
};
|
|
160
|
-
var logInfo = (config) => {
|
|
161
|
-
if (config.verbose === false) {
|
|
162
|
-
return noop;
|
|
163
|
-
}
|
|
164
|
-
if (config.logFunction) {
|
|
165
|
-
return config.logFunction;
|
|
166
|
-
}
|
|
167
|
-
return (text, overwrite) => {
|
|
168
|
-
if (overwrite === true && process.stdout.isTTY) {
|
|
169
|
-
clearLine(process.stdout, 0);
|
|
170
|
-
cursorTo(process.stdout, 0);
|
|
171
|
-
} else {
|
|
172
|
-
process.stdout.write("\n");
|
|
173
|
-
}
|
|
174
|
-
process.stdout.write(text);
|
|
175
|
-
};
|
|
176
|
-
};
|
|
177
|
-
var logWarn = (config) => {
|
|
178
|
-
if (config.logFunction) {
|
|
179
|
-
return config.logFunction;
|
|
180
|
-
}
|
|
181
|
-
return (text) => {
|
|
182
|
-
process.stdout.write(`
|
|
183
|
-
${formatWarning(text)}
|
|
184
|
-
`);
|
|
185
|
-
};
|
|
186
|
-
};
|
|
187
|
-
var logError = (config) => {
|
|
188
|
-
if (config.logFunction) {
|
|
189
|
-
return config.logFunction;
|
|
190
|
-
}
|
|
191
|
-
return (text) => {
|
|
192
|
-
process.stdout.write(`
|
|
193
|
-
${formatError(text)}
|
|
194
|
-
`);
|
|
195
|
-
};
|
|
196
|
-
};
|
|
197
|
-
function createLogger(config) {
|
|
198
|
-
return {
|
|
199
|
-
info: logInfo(config),
|
|
200
|
-
warn: logWarn(config),
|
|
201
|
-
error: logError(config)
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// src/lib/transit-departures-widget.ts
|
|
206
|
-
import path from "path";
|
|
207
|
-
import { clone, omit } from "lodash-es";
|
|
208
|
-
import { writeFile } from "fs/promises";
|
|
209
|
-
import { importGtfs, openDb as openDb2 } from "gtfs";
|
|
210
|
-
import sanitize from "sanitize-filename";
|
|
211
|
-
import Timer from "timer-machine";
|
|
212
|
-
import untildify2 from "untildify";
|
|
213
|
-
|
|
214
|
-
// src/lib/config/defaults.ts
|
|
215
|
-
import { join as join2 } from "path";
|
|
216
|
-
import { I18n } from "i18n";
|
|
217
|
-
function setDefaultConfig(initialConfig) {
|
|
218
|
-
const defaults = {
|
|
219
|
-
beautify: false,
|
|
220
|
-
noHead: false,
|
|
221
|
-
refreshIntervalSeconds: 20,
|
|
222
|
-
skipImport: false,
|
|
223
|
-
timeFormat: "12hour",
|
|
224
|
-
includeCoordinates: false,
|
|
225
|
-
overwriteExistingFiles: true,
|
|
226
|
-
verbose: true
|
|
227
|
-
};
|
|
228
|
-
const config = Object.assign(defaults, initialConfig);
|
|
229
|
-
const viewsFolderPath = getPathToViewsFolder(config);
|
|
230
|
-
const i18n = new I18n({
|
|
231
|
-
directory: join2(viewsFolderPath, "locales"),
|
|
232
|
-
defaultLocale: config.locale,
|
|
233
|
-
updateFiles: false
|
|
234
|
-
});
|
|
235
|
-
const configWithI18n = Object.assign(config, {
|
|
236
|
-
__: i18n.__
|
|
237
|
-
});
|
|
238
|
-
return configWithI18n;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// src/lib/utils.ts
|
|
242
|
-
import { openDb, getDirections, getRoutes, getStops, getTrips } from "gtfs";
|
|
243
|
-
import { groupBy, last, maxBy, size, sortBy, uniqBy } from "lodash-es";
|
|
244
|
-
import sqlString from "sqlstring-sqlite";
|
|
245
|
-
import toposort from "toposort";
|
|
246
|
-
|
|
247
|
-
// src/lib/logging/messages.ts
|
|
248
|
-
var messages = {
|
|
249
|
-
noActiveCalendarsGlobal: "No active calendars found for the configured date range - returning empty routes and stops",
|
|
250
|
-
noActiveCalendarsForRoute: (routeId) => `route_id ${routeId} has no active calendars in range - skipping directions`,
|
|
251
|
-
noActiveCalendarsForDirection: (routeId, directionId) => `route_id ${routeId} direction ${directionId} has no active calendars in range - skipping stops`,
|
|
252
|
-
routeHasNoDirections: (routeId) => `route_id ${routeId} has no directions - skipping`,
|
|
253
|
-
stopNotFound: (routeId, directionId, stopId) => `stop_id ${stopId} for route ${routeId} direction ${directionId} not found - dropping`
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// src/lib/utils.ts
|
|
257
|
-
var getCalendarsForDateRange = (config) => {
|
|
258
|
-
const db = openDb(config);
|
|
259
|
-
let whereClause = "";
|
|
260
|
-
const whereClauses = [];
|
|
261
|
-
if (config.endDate) {
|
|
262
|
-
whereClauses.push(`start_date <= ${sqlString.escape(config.endDate)}`);
|
|
263
|
-
}
|
|
264
|
-
if (config.startDate) {
|
|
265
|
-
whereClauses.push(`end_date >= ${sqlString.escape(config.startDate)}`);
|
|
266
|
-
}
|
|
267
|
-
if (whereClauses.length > 0) {
|
|
268
|
-
whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
269
|
-
}
|
|
270
|
-
return db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
|
|
271
|
-
};
|
|
272
|
-
function formatRouteName(route) {
|
|
273
|
-
let routeName = "";
|
|
274
|
-
if (route.route_short_name !== null) {
|
|
275
|
-
routeName += route.route_short_name;
|
|
276
|
-
}
|
|
277
|
-
if (route.route_short_name !== null && route.route_long_name !== null) {
|
|
278
|
-
routeName += " - ";
|
|
279
|
-
}
|
|
280
|
-
if (route.route_long_name !== null) {
|
|
281
|
-
routeName += route.route_long_name;
|
|
282
|
-
}
|
|
283
|
-
return routeName;
|
|
284
|
-
}
|
|
285
|
-
function getDirectionsForRoute(route, config) {
|
|
286
|
-
const logger = createLogger(config);
|
|
287
|
-
const db = openDb(config);
|
|
288
|
-
const directions = getDirections({ route_id: route.route_id }, [
|
|
289
|
-
"direction_id",
|
|
290
|
-
"direction"
|
|
291
|
-
]).filter((direction) => direction.direction_id !== void 0).map((direction) => ({
|
|
292
|
-
direction_id: direction.direction_id,
|
|
293
|
-
direction: direction.direction
|
|
294
|
-
}));
|
|
295
|
-
const calendars = getCalendarsForDateRange(config);
|
|
296
|
-
if (calendars.length === 0) {
|
|
297
|
-
logger.warn(messages.noActiveCalendarsForRoute(route.route_id));
|
|
298
|
-
return [];
|
|
299
|
-
}
|
|
300
|
-
if (directions.length === 0) {
|
|
301
|
-
const headsigns = db.prepare(
|
|
302
|
-
`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`
|
|
303
|
-
).all(route.route_id);
|
|
304
|
-
for (const group of Object.values(groupBy(headsigns, "direction_id"))) {
|
|
305
|
-
const mostCommonHeadsign = maxBy(group, "count");
|
|
306
|
-
directions.push({
|
|
307
|
-
direction_id: mostCommonHeadsign.direction_id,
|
|
308
|
-
direction: config.__("To {{{headsign}}}", {
|
|
309
|
-
headsign: mostCommonHeadsign.trip_headsign
|
|
310
|
-
})
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return directions;
|
|
315
|
-
}
|
|
316
|
-
function sortStopIdsBySequence(stoptimes) {
|
|
317
|
-
const stoptimesGroupedByTrip = groupBy(stoptimes, "trip_id");
|
|
318
|
-
try {
|
|
319
|
-
const stopGraph = [];
|
|
320
|
-
for (const tripStoptimes of Object.values(stoptimesGroupedByTrip)) {
|
|
321
|
-
const sortedStopIds = sortBy(tripStoptimes, "stop_sequence").map(
|
|
322
|
-
(stoptime) => stoptime.stop_id
|
|
323
|
-
);
|
|
324
|
-
for (const [index, stopId] of sortedStopIds.entries()) {
|
|
325
|
-
if (index === sortedStopIds.length - 1) {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
stopGraph.push([stopId, sortedStopIds[index + 1]]);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return toposort(
|
|
332
|
-
stopGraph
|
|
333
|
-
);
|
|
334
|
-
} catch {
|
|
335
|
-
}
|
|
336
|
-
const longestTripStoptimes = maxBy(
|
|
337
|
-
Object.values(stoptimesGroupedByTrip),
|
|
338
|
-
(stoptimes2) => size(stoptimes2)
|
|
339
|
-
);
|
|
340
|
-
if (!longestTripStoptimes) {
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
return longestTripStoptimes.map((stoptime) => stoptime.stop_id);
|
|
344
|
-
}
|
|
345
|
-
function getStopsForDirection(route, direction, config, stopCache) {
|
|
346
|
-
const logger = createLogger(config);
|
|
347
|
-
const db = openDb(config);
|
|
348
|
-
const calendars = getCalendarsForDateRange(config);
|
|
349
|
-
if (calendars.length === 0) {
|
|
350
|
-
logger.warn(
|
|
351
|
-
messages.noActiveCalendarsForDirection(
|
|
352
|
-
route.route_id,
|
|
353
|
-
direction.direction_id
|
|
354
|
-
)
|
|
355
|
-
);
|
|
356
|
-
return [];
|
|
357
|
-
}
|
|
358
|
-
const whereClause = formatWhereClauses({
|
|
359
|
-
direction_id: direction.direction_id,
|
|
360
|
-
route_id: route.route_id,
|
|
361
|
-
service_id: calendars.map((calendar) => calendar.service_id)
|
|
362
|
-
});
|
|
363
|
-
const stoptimes = db.prepare(
|
|
364
|
-
`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`
|
|
365
|
-
).all();
|
|
366
|
-
const sortedStopIds = sortStopIdsBySequence(stoptimes);
|
|
367
|
-
const deduplicatedStopIds = sortedStopIds.reduce(
|
|
368
|
-
(memo, stopId) => {
|
|
369
|
-
if (last(memo) !== stopId) {
|
|
370
|
-
memo.push(stopId);
|
|
371
|
-
}
|
|
372
|
-
return memo;
|
|
373
|
-
},
|
|
374
|
-
[]
|
|
375
|
-
);
|
|
376
|
-
deduplicatedStopIds.pop();
|
|
377
|
-
const stopFields = [
|
|
378
|
-
"stop_id",
|
|
379
|
-
"stop_name",
|
|
380
|
-
"stop_code",
|
|
381
|
-
"parent_station"
|
|
382
|
-
];
|
|
383
|
-
if (config.includeCoordinates) {
|
|
384
|
-
stopFields.push("stop_lat", "stop_lon");
|
|
385
|
-
}
|
|
386
|
-
const missingStopIds = stopCache ? deduplicatedStopIds.filter((stopId) => !stopCache.has(stopId)) : deduplicatedStopIds;
|
|
387
|
-
const fetchedStops = missingStopIds.length ? getStops(
|
|
388
|
-
{ stop_id: missingStopIds },
|
|
389
|
-
stopFields
|
|
390
|
-
) : [];
|
|
391
|
-
if (stopCache) {
|
|
392
|
-
for (const stop of fetchedStops) {
|
|
393
|
-
stopCache.set(stop.stop_id, stop);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return deduplicatedStopIds.map((stopId) => {
|
|
397
|
-
const stop = stopCache?.get(stopId) ?? fetchedStops.find((candidate) => candidate.stop_id === stopId);
|
|
398
|
-
if (!stop) {
|
|
399
|
-
logger.warn(
|
|
400
|
-
messages.stopNotFound(route.route_id, direction.direction_id, stopId)
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
return stop;
|
|
404
|
-
}).filter(Boolean);
|
|
405
|
-
}
|
|
406
|
-
function generateTransitDeparturesWidgetHtml(config) {
|
|
407
|
-
const templateVars = {
|
|
408
|
-
config,
|
|
409
|
-
__: config.__
|
|
410
|
-
};
|
|
411
|
-
return renderFile("widget", templateVars, config);
|
|
412
|
-
}
|
|
413
|
-
function generateTransitDeparturesWidgetJson(config) {
|
|
414
|
-
const logger = createLogger(config);
|
|
415
|
-
const calendars = getCalendarsForDateRange(config);
|
|
416
|
-
if (calendars.length === 0) {
|
|
417
|
-
logger.warn(messages.noActiveCalendarsGlobal);
|
|
418
|
-
return { routes: [], stops: [] };
|
|
419
|
-
}
|
|
420
|
-
const routes = getRoutes();
|
|
421
|
-
const stops = [];
|
|
422
|
-
const filteredRoutes = [];
|
|
423
|
-
const stopCache = /* @__PURE__ */ new Map();
|
|
424
|
-
for (const route of routes) {
|
|
425
|
-
const routeWithFullName = {
|
|
426
|
-
...route,
|
|
427
|
-
route_full_name: formatRouteName(route)
|
|
428
|
-
};
|
|
429
|
-
const directions = getDirectionsForRoute(routeWithFullName, config);
|
|
430
|
-
if (directions.length === 0) {
|
|
431
|
-
logger.warn(messages.routeHasNoDirections(route.route_id));
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
const directionsWithData = directions.map((direction) => {
|
|
435
|
-
const directionStops = getStopsForDirection(
|
|
436
|
-
routeWithFullName,
|
|
437
|
-
direction,
|
|
438
|
-
config,
|
|
439
|
-
stopCache
|
|
440
|
-
);
|
|
441
|
-
if (directionStops.length === 0) {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
stops.push(...directionStops);
|
|
445
|
-
const trips = getTrips(
|
|
446
|
-
{
|
|
447
|
-
route_id: route.route_id,
|
|
448
|
-
direction_id: direction.direction_id,
|
|
449
|
-
service_id: calendars.map(
|
|
450
|
-
(calendar) => calendar.service_id
|
|
451
|
-
)
|
|
452
|
-
},
|
|
453
|
-
["trip_id"]
|
|
454
|
-
);
|
|
455
|
-
return {
|
|
456
|
-
...direction,
|
|
457
|
-
stopIds: directionStops.map((stop) => stop.stop_id),
|
|
458
|
-
tripIds: trips.map((trip) => trip.trip_id)
|
|
459
|
-
};
|
|
460
|
-
}).filter(Boolean);
|
|
461
|
-
if (directionsWithData.length === 0) {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
filteredRoutes.push({
|
|
465
|
-
...routeWithFullName,
|
|
466
|
-
directions: directionsWithData
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
const sortedRoutes = [...filteredRoutes].sort((a, b) => {
|
|
470
|
-
const aShort = a.route_short_name ?? "";
|
|
471
|
-
const bShort = b.route_short_name ?? "";
|
|
472
|
-
const aNum = Number.parseInt(aShort, 10);
|
|
473
|
-
const bNum = Number.parseInt(bShort, 10);
|
|
474
|
-
if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && aNum !== bNum) {
|
|
475
|
-
return aNum - bNum;
|
|
476
|
-
}
|
|
477
|
-
if (Number.isNaN(aNum) && !Number.isNaN(bNum)) {
|
|
478
|
-
return 1;
|
|
479
|
-
}
|
|
480
|
-
if (!Number.isNaN(aNum) && Number.isNaN(bNum)) {
|
|
481
|
-
return -1;
|
|
482
|
-
}
|
|
483
|
-
return aShort.localeCompare(bShort, void 0, {
|
|
484
|
-
numeric: true,
|
|
485
|
-
sensitivity: "base"
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
const parentStationIds = new Set(stops.map((stop) => stop.parent_station));
|
|
489
|
-
const parentStationStops = getStops(
|
|
490
|
-
{ stop_id: Array.from(parentStationIds) },
|
|
491
|
-
["stop_id", "stop_name", "stop_code", "parent_station"]
|
|
492
|
-
);
|
|
493
|
-
stops.push(
|
|
494
|
-
...parentStationStops.map((stop) => ({
|
|
495
|
-
...stop,
|
|
496
|
-
is_parent_station: true
|
|
497
|
-
}))
|
|
498
|
-
);
|
|
499
|
-
const sortedStops = sortBy(uniqBy(stops, "stop_id"), "stop_name");
|
|
500
|
-
return {
|
|
501
|
-
routes: arrayOfArrays(removeNulls(sortedRoutes)),
|
|
502
|
-
stops: arrayOfArrays(removeNulls(sortedStops))
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
function removeNulls(data) {
|
|
506
|
-
if (Array.isArray(data)) {
|
|
507
|
-
return data.map(removeNulls).filter((item) => item !== null && item !== void 0);
|
|
508
|
-
} else if (data !== null && typeof data === "object" && Object.getPrototypeOf(data) === Object.prototype) {
|
|
509
|
-
return Object.entries(data).reduce(
|
|
510
|
-
(acc, [key, value]) => {
|
|
511
|
-
const cleanedValue = removeNulls(value);
|
|
512
|
-
if (cleanedValue !== null && cleanedValue !== void 0) {
|
|
513
|
-
acc[key] = cleanedValue;
|
|
514
|
-
}
|
|
515
|
-
return acc;
|
|
516
|
-
},
|
|
517
|
-
{}
|
|
518
|
-
);
|
|
519
|
-
} else {
|
|
520
|
-
return data;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
function arrayOfArrays(array) {
|
|
524
|
-
if (array.length === 0) {
|
|
525
|
-
return { fields: [], rows: [] };
|
|
526
|
-
}
|
|
527
|
-
const fields = Array.from(
|
|
528
|
-
array.reduce((fieldSet, item) => {
|
|
529
|
-
Object.keys(item ?? {}).forEach((key) => fieldSet.add(key));
|
|
530
|
-
return fieldSet;
|
|
531
|
-
}, /* @__PURE__ */ new Set())
|
|
532
|
-
);
|
|
533
|
-
return {
|
|
534
|
-
fields,
|
|
535
|
-
rows: array.map((item) => fields.map((field) => item?.[field] ?? null))
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
function formatWhereClause(key, value) {
|
|
539
|
-
if (Array.isArray(value)) {
|
|
540
|
-
let whereClause = `${sqlString.escapeId(key)} IN (${value.filter((v) => v !== null).map((v) => sqlString.escape(v)).join(", ")})`;
|
|
541
|
-
if (value.includes(null)) {
|
|
542
|
-
whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`;
|
|
543
|
-
}
|
|
544
|
-
return whereClause;
|
|
545
|
-
}
|
|
546
|
-
if (value === null) {
|
|
547
|
-
return `${sqlString.escapeId(key)} IS NULL`;
|
|
548
|
-
}
|
|
549
|
-
return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`;
|
|
550
|
-
}
|
|
551
|
-
function formatWhereClauses(query) {
|
|
552
|
-
if (Object.keys(query).length === 0) {
|
|
553
|
-
return "";
|
|
554
|
-
}
|
|
555
|
-
const whereClauses = Object.entries(query).map(
|
|
556
|
-
([key, value]) => formatWhereClause(key, value)
|
|
557
|
-
);
|
|
558
|
-
return `WHERE ${whereClauses.join(" AND ")}`;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// src/lib/transit-departures-widget.ts
|
|
562
|
-
async function transitDeparturesWidget(initialConfig) {
|
|
563
|
-
const config = setDefaultConfig(initialConfig);
|
|
564
|
-
const logger = createLogger(config);
|
|
565
|
-
try {
|
|
566
|
-
openDb2(config);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
if (error?.code === "SQLITE_CANTOPEN") {
|
|
569
|
-
logger.error(
|
|
570
|
-
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
throw error;
|
|
574
|
-
}
|
|
575
|
-
if (!config.agency) {
|
|
576
|
-
throw new Error("No agency defined in `config.json`");
|
|
577
|
-
}
|
|
578
|
-
const timer = new Timer();
|
|
579
|
-
const agencyKey = config.agency.agency_key ?? "unknown";
|
|
580
|
-
const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize(agencyKey));
|
|
581
|
-
timer.start();
|
|
582
|
-
if (!config.skipImport) {
|
|
583
|
-
const gtfsPath = config.agency.gtfs_static_path;
|
|
584
|
-
const gtfsUrl = config.agency.gtfs_static_url;
|
|
585
|
-
if (!gtfsPath && !gtfsUrl) {
|
|
586
|
-
throw new Error(
|
|
587
|
-
"Missing GTFS source. Set `agency.gtfs_static_path` or `agency.gtfs_static_url` in config.json."
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
const agencyImportConfig = gtfsPath ? { path: gtfsPath } : { url: gtfsUrl };
|
|
591
|
-
const gtfsImportConfig = {
|
|
592
|
-
...clone(omit(config, "agency")),
|
|
593
|
-
agencies: [agencyImportConfig]
|
|
594
|
-
};
|
|
595
|
-
await importGtfs(gtfsImportConfig);
|
|
596
|
-
}
|
|
597
|
-
await prepDirectory(outputPath, config);
|
|
598
|
-
if (config.noHead !== true) {
|
|
599
|
-
await copyStaticAssets(config, outputPath);
|
|
600
|
-
}
|
|
601
|
-
logger.info(`${agencyKey}: Generating Transit Departures Widget HTML`);
|
|
602
|
-
config.assetPath = "";
|
|
603
|
-
const { routes, stops } = generateTransitDeparturesWidgetJson(config);
|
|
604
|
-
await writeFile(
|
|
605
|
-
path.join(outputPath, "data", "routes.json"),
|
|
606
|
-
JSON.stringify(routes)
|
|
607
|
-
);
|
|
608
|
-
await writeFile(
|
|
609
|
-
path.join(outputPath, "data", "stops.json"),
|
|
610
|
-
JSON.stringify(stops)
|
|
611
|
-
);
|
|
612
|
-
const html = await generateTransitDeparturesWidgetHtml(config);
|
|
613
|
-
await writeFile(path.join(outputPath, "index.html"), html);
|
|
614
|
-
timer.stop();
|
|
615
|
-
logger.info(
|
|
616
|
-
`${agencyKey}: Transit Departures Widget HTML created at ${outputPath}`
|
|
617
|
-
);
|
|
618
|
-
const seconds = Math.round(timer.time() / 1e3);
|
|
619
|
-
logger.info(`${agencyKey}: HTML generation required ${seconds} seconds`);
|
|
620
|
-
}
|
|
621
|
-
var transit_departures_widget_default = transitDeparturesWidget;
|
|
622
|
-
|
|
623
|
-
// src/bin/transit-departures-widget.ts
|
|
624
|
-
var pe = new PrettyError();
|
|
625
|
-
var argv = yargs(hideBin(process.argv)).usage("Usage: $0 --config ./config.json").help().option("c", {
|
|
626
|
-
alias: "configPath",
|
|
627
|
-
describe: "Path to config file",
|
|
628
|
-
default: "./config.json",
|
|
629
|
-
type: "string"
|
|
8
|
+
//#region src/bin/transit-departures-widget.ts
|
|
9
|
+
const pe = new PrettyError();
|
|
10
|
+
const argv = yargs(hideBin(process.argv)).usage("Usage: $0 --config ./config.json").help().option("c", {
|
|
11
|
+
alias: "configPath",
|
|
12
|
+
describe: "Path to config file",
|
|
13
|
+
default: "./config.json",
|
|
14
|
+
type: "string"
|
|
630
15
|
}).option("s", {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
16
|
+
alias: "skipImport",
|
|
17
|
+
describe: "Don’t import GTFS file.",
|
|
18
|
+
type: "boolean"
|
|
634
19
|
}).default("skipImport", void 0).parseSync();
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
console.error(pe.render(error));
|
|
641
|
-
process.exit(1);
|
|
20
|
+
const handleError = (error) => {
|
|
21
|
+
const text = error || "Unknown Error";
|
|
22
|
+
process.stdout.write(`\n${formatError(text)}\n`);
|
|
23
|
+
console.error(pe.render(error));
|
|
24
|
+
process.exit(1);
|
|
642
25
|
};
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
process.exit();
|
|
26
|
+
const setupImport = async () => {
|
|
27
|
+
await transitDeparturesWidget(await getConfig(argv));
|
|
28
|
+
process.exit();
|
|
647
29
|
};
|
|
648
30
|
setupImport().catch(handleError);
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { };
|
|
649
34
|
//# sourceMappingURL=transit-departures-widget.js.map
|