gtfs 4.19.0 → 4.19.2
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 +2 -0
- package/dist/bin/gtfs-export.d.ts +1 -1
- package/dist/bin/gtfs-export.js +19 -4378
- package/dist/bin/gtfs-export.js.map +1 -1
- package/dist/bin/gtfs-import.d.ts +1 -1
- package/dist/bin/gtfs-import.js +27 -5354
- package/dist/bin/gtfs-import.js.map +1 -1
- package/dist/bin/gtfsrealtime-update.d.ts +1 -1
- package/dist/bin/gtfsrealtime-update.js +17 -1186
- package/dist/bin/gtfsrealtime-update.js.map +1 -1
- package/dist/index.d.ts +984 -732
- package/dist/index.js +2 -6856
- package/dist/models/models.d.ts +2646 -2578
- package/dist/models/models.js +2 -3981
- package/dist/models-9NvwLHlL.js +4044 -0
- package/dist/models-9NvwLHlL.js.map +1 -0
- package/dist/src-CdVKeHzG.js +2476 -0
- package/dist/src-CdVKeHzG.js.map +1 -0
- package/package.json +14 -14
- package/dist/index.js.map +0 -1
- package/dist/models/models.js.map +0 -1
|
@@ -0,0 +1,2476 @@
|
|
|
1
|
+
import { S as tripUpdates, b as vehiclePositions, t as models_exports, v as serviceAlertInformedEntities, x as stopTimeUpdates, y as serviceAlerts } from "./models-9NvwLHlL.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createReadStream, existsSync, lstatSync } from "node:fs";
|
|
4
|
+
import { cp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { parse } from "csv-parse";
|
|
6
|
+
import stripBomStream from "strip-bom-stream";
|
|
7
|
+
import { temporaryDirectory } from "tempy";
|
|
8
|
+
import mapSeries from "promise-map-series";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import Database$1 from "better-sqlite3";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { cloneDeep, compact, filter, get, groupBy, last, noop, omit, omitBy, orderBy, pick, snakeCase, sortBy, without } from "lodash-es";
|
|
13
|
+
import sanitize from "sanitize-filename";
|
|
14
|
+
import StreamZip from "node-stream-zip";
|
|
15
|
+
import { clearLine, cursorTo } from "node:readline";
|
|
16
|
+
import * as colors from "yoctocolors";
|
|
17
|
+
import { feature, featureCollection } from "@turf/helpers";
|
|
18
|
+
import GtfsRealtimeBindings from "gtfs-realtime-bindings";
|
|
19
|
+
import sqlString from "sqlstring-sqlite";
|
|
20
|
+
import Long from "long";
|
|
21
|
+
import { stringify } from "csv-stringify";
|
|
22
|
+
|
|
23
|
+
//#region src/lib/log-utils.ts
|
|
24
|
+
/**
|
|
25
|
+
* Creates a logging function based on configuration settings
|
|
26
|
+
* @param {Config} config - Configuration object containing logging preferences
|
|
27
|
+
* @returns {LogFunction} Logging function that writes to stdout, or noop if verbose is false
|
|
28
|
+
* @example
|
|
29
|
+
* const logger = log({ verbose: true });
|
|
30
|
+
* logger('Processing...', true); // Overwrites current line
|
|
31
|
+
* logger('Done!'); // Writes on new line
|
|
32
|
+
*/
|
|
33
|
+
function log(config) {
|
|
34
|
+
if (config.verbose === false) return noop;
|
|
35
|
+
if (config.logFunction) return config.logFunction;
|
|
36
|
+
return (text, overwrite = false) => {
|
|
37
|
+
if (overwrite && process.stdout.isTTY) {
|
|
38
|
+
clearLine(process.stdout, 0);
|
|
39
|
+
cursorTo(process.stdout, 0);
|
|
40
|
+
} else process.stdout.write("\n");
|
|
41
|
+
process.stdout.write(text);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a warning logging function
|
|
46
|
+
* @param {Config} config - Configuration object containing logging preferences
|
|
47
|
+
* @returns {(text: string) => void} Function that logs formatted warning messages
|
|
48
|
+
* @example
|
|
49
|
+
* const warnLogger = logWarning(config);
|
|
50
|
+
* warnLogger('Resource not found'); // Outputs yellow warning message
|
|
51
|
+
*/
|
|
52
|
+
function logWarning(config) {
|
|
53
|
+
if (config.logFunction) return config.logFunction;
|
|
54
|
+
return (text) => {
|
|
55
|
+
process.stdout.write(`\n${formatWarning(text)}\n`);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Creates an error logging function
|
|
60
|
+
* @param {Config} config - Configuration object containing logging preferences
|
|
61
|
+
* @returns {(text: string) => void} Function that logs formatted error messages
|
|
62
|
+
* @example
|
|
63
|
+
* const errorLogger = logError(config);
|
|
64
|
+
* errorLogger('Failed to connect'); // Outputs red error message
|
|
65
|
+
*/
|
|
66
|
+
function logError(config) {
|
|
67
|
+
if (config.logFunction) return config.logFunction;
|
|
68
|
+
return (text) => {
|
|
69
|
+
process.stdout.write(`\n${formatError(text)}\n`);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Formats warning text with yellow color and underline
|
|
74
|
+
* @param {string} text - The warning message to format
|
|
75
|
+
* @returns {string} Formatted warning message in yellow with underlined "Warning" prefix
|
|
76
|
+
* @example
|
|
77
|
+
* const formattedWarning = formatWarning('Resource not found');
|
|
78
|
+
* console.log(formattedWarning); // Yellow "Warning: Resource not found"
|
|
79
|
+
*/
|
|
80
|
+
function formatWarning(text) {
|
|
81
|
+
return colors.yellow(`${colors.underline("Warning")}: ${text}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Formats error text with red color and underline
|
|
85
|
+
* @param {Error | string} error - The error object or message to format
|
|
86
|
+
* @returns {string} Formatted error message in red with underlined "Error" prefix
|
|
87
|
+
* @example
|
|
88
|
+
* const formattedError = formatError(new Error('Connection failed'));
|
|
89
|
+
* console.log(formattedError); // Red "Error: Connection failed"
|
|
90
|
+
*/
|
|
91
|
+
function formatError(error) {
|
|
92
|
+
const cleanMessage = (error instanceof Error ? error.message : error).replace(/^Error:\s*/i, "");
|
|
93
|
+
return colors.red(`${colors.underline("Error")}: ${cleanMessage}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/lib/file-utils.ts
|
|
98
|
+
const homeDirectory = homedir();
|
|
99
|
+
/**
|
|
100
|
+
* Attempts to parse and load configuration from various sources
|
|
101
|
+
* Priority: 1. CLI config path 2. CLI direct args 3. ./config.json
|
|
102
|
+
* @param {ConfigArgs} argv - Command line arguments
|
|
103
|
+
* @throws {Error} If configuration cannot be found or parsed
|
|
104
|
+
* @returns {Promise<Record<string, any>>} Parsed configuration object
|
|
105
|
+
* @example
|
|
106
|
+
* const config = await getConfig({ configPath: './my-config.json' });
|
|
107
|
+
*/
|
|
108
|
+
async function getConfig(argv) {
|
|
109
|
+
let config;
|
|
110
|
+
let data;
|
|
111
|
+
try {
|
|
112
|
+
if (argv.configPath) {
|
|
113
|
+
data = await readFile(path.resolve(untildify(argv.configPath)), "utf8");
|
|
114
|
+
config = Object.assign(JSON.parse(data), argv);
|
|
115
|
+
} else if (argv.gtfsPath || argv.gtfsUrl || argv.sqlitePath) config = {
|
|
116
|
+
agencies: [...argv.gtfsPath ? [{ path: argv.gtfsPath }] : [], ...argv.gtfsUrl ? [{ url: argv.gtfsUrl }] : []],
|
|
117
|
+
...omit(argv, ["path", "url"])
|
|
118
|
+
};
|
|
119
|
+
else if (existsSync(path.resolve("./config.json"))) {
|
|
120
|
+
data = await readFile(path.resolve("./config.json"), "utf8");
|
|
121
|
+
config = Object.assign(JSON.parse(data), argv);
|
|
122
|
+
log(config)("Using configuration from ./config.json");
|
|
123
|
+
} else throw new Error("Cannot find configuration file. Use config-sample.json as a starting point, pass --configPath option.");
|
|
124
|
+
return config;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof SyntaxError) throw new Error(`Cannot parse configuration file. Check to ensure that it is valid JSON. Error: ${error.message}`, { cause: error });
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Prepares a directory for saving files by clearing its contents
|
|
132
|
+
* @param {string} exportPath - Path to the directory to prepare
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
* @example
|
|
135
|
+
* await prepDirectory('./output');
|
|
136
|
+
*/
|
|
137
|
+
async function prepDirectory(exportPath) {
|
|
138
|
+
await rm(exportPath, {
|
|
139
|
+
recursive: true,
|
|
140
|
+
force: true
|
|
141
|
+
});
|
|
142
|
+
await mkdir(exportPath, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Extracts contents of a zip file to specified directory
|
|
146
|
+
* @param {string} zipfilePath - Path to the zip file
|
|
147
|
+
* @param {string} exportPath - Directory to extract contents to
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
* @throws {Error} If zip file cannot be opened or extracted
|
|
150
|
+
* @example
|
|
151
|
+
* await unzip('./data.zip', './extracted');
|
|
152
|
+
*/
|
|
153
|
+
async function unzip(zipfilePath, exportPath) {
|
|
154
|
+
try {
|
|
155
|
+
const zip = new StreamZip.async({ file: zipfilePath });
|
|
156
|
+
await zip.extract(null, exportPath);
|
|
157
|
+
await zip.close();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(`Failed to extract zip file: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Generates a safe folder name from input string
|
|
164
|
+
* Converts to snake_case and removes unsafe characters
|
|
165
|
+
* @param {string} folderName - Input string to convert to folder name
|
|
166
|
+
* @returns {string} Sanitized folder name
|
|
167
|
+
* @example
|
|
168
|
+
* generateFolderName('My Folder!') // returns 'my_folder'
|
|
169
|
+
*/
|
|
170
|
+
function generateFolderName(folderName) {
|
|
171
|
+
if (!folderName || typeof folderName !== "string") throw new Error("Folder name must be a non-empty string");
|
|
172
|
+
return snakeCase(sanitize(folderName));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Converts a tilde path to a full path
|
|
176
|
+
* @param pathWithTilde The path to convert
|
|
177
|
+
* @returns The full path
|
|
178
|
+
*/
|
|
179
|
+
function untildify(pathWithTilde) {
|
|
180
|
+
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/lib/errors.ts
|
|
185
|
+
let GtfsErrorCategory = /* @__PURE__ */ function(GtfsErrorCategory) {
|
|
186
|
+
GtfsErrorCategory["CONFIG"] = "config";
|
|
187
|
+
GtfsErrorCategory["DOWNLOAD"] = "download";
|
|
188
|
+
GtfsErrorCategory["ZIP"] = "zip";
|
|
189
|
+
GtfsErrorCategory["VALIDATION"] = "validation";
|
|
190
|
+
GtfsErrorCategory["DATABASE"] = "database";
|
|
191
|
+
GtfsErrorCategory["PARSE"] = "parse";
|
|
192
|
+
GtfsErrorCategory["QUERY"] = "query";
|
|
193
|
+
GtfsErrorCategory["INTERNAL"] = "internal";
|
|
194
|
+
return GtfsErrorCategory;
|
|
195
|
+
}({});
|
|
196
|
+
/**
|
|
197
|
+
* Error codes are a public API contract and must remain stable across
|
|
198
|
+
* minor/patch releases.
|
|
199
|
+
*/
|
|
200
|
+
let GtfsErrorCode = /* @__PURE__ */ function(GtfsErrorCode) {
|
|
201
|
+
GtfsErrorCode["GTFS_DOWNLOAD_HTTP"] = "GTFS_DOWNLOAD_HTTP";
|
|
202
|
+
GtfsErrorCode["GTFS_DOWNLOAD_FAILED"] = "GTFS_DOWNLOAD_FAILED";
|
|
203
|
+
GtfsErrorCode["GTFS_ZIP_INVALID"] = "GTFS_ZIP_INVALID";
|
|
204
|
+
GtfsErrorCode["GTFS_REQUIRED_FIELD_MISSING"] = "GTFS_REQUIRED_FIELD_MISSING";
|
|
205
|
+
GtfsErrorCode["GTFS_INVALID_DATE"] = "GTFS_INVALID_DATE";
|
|
206
|
+
GtfsErrorCode["GTFS_CONFIG_INVALID"] = "GTFS_CONFIG_INVALID";
|
|
207
|
+
GtfsErrorCode["DB_OPEN_FAILED"] = "DB_OPEN_FAILED";
|
|
208
|
+
GtfsErrorCode["GTFS_DB_OPERATION_FAILED"] = "GTFS_DB_OPERATION_FAILED";
|
|
209
|
+
GtfsErrorCode["GTFS_JSON_INVALID"] = "GTFS_JSON_INVALID";
|
|
210
|
+
GtfsErrorCode["GTFS_UNSUPPORTED_FILE_TYPE"] = "GTFS_UNSUPPORTED_FILE_TYPE";
|
|
211
|
+
GtfsErrorCode["GTFS_CSV_PARSE_FAILED"] = "GTFS_CSV_PARSE_FAILED";
|
|
212
|
+
GtfsErrorCode["GTFS_QUERY_INVALID"] = "GTFS_QUERY_INVALID";
|
|
213
|
+
return GtfsErrorCode;
|
|
214
|
+
}({});
|
|
215
|
+
let GtfsWarningCode = /* @__PURE__ */ function(GtfsWarningCode) {
|
|
216
|
+
GtfsWarningCode["GTFS_DUPLICATE_PRIMARY_KEY"] = "GTFS_DUPLICATE_PRIMARY_KEY";
|
|
217
|
+
return GtfsWarningCode;
|
|
218
|
+
}({});
|
|
219
|
+
var GtfsError = class extends Error {
|
|
220
|
+
code;
|
|
221
|
+
category;
|
|
222
|
+
isOperational;
|
|
223
|
+
statusCode;
|
|
224
|
+
details;
|
|
225
|
+
constructor(message, options) {
|
|
226
|
+
super(message, { cause: options.cause });
|
|
227
|
+
this.name = "GtfsError";
|
|
228
|
+
this.code = options.code;
|
|
229
|
+
this.category = options.category;
|
|
230
|
+
this.isOperational = options.isOperational ?? true;
|
|
231
|
+
this.statusCode = options.statusCode;
|
|
232
|
+
this.details = options.details;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
function isGtfsError(error) {
|
|
236
|
+
if (!error || typeof error !== "object") return false;
|
|
237
|
+
const candidate = error;
|
|
238
|
+
return candidate.name === "GtfsError" && typeof candidate.message === "string" && typeof candidate.code === "string" && typeof candidate.category === "string" && typeof candidate.isOperational === "boolean";
|
|
239
|
+
}
|
|
240
|
+
function isGtfsValidationError(error) {
|
|
241
|
+
return isGtfsError(error) && error.category === "validation";
|
|
242
|
+
}
|
|
243
|
+
function toGtfsError(error, fallback) {
|
|
244
|
+
if (isGtfsError(error)) return error;
|
|
245
|
+
return new GtfsError(fallback.message, {
|
|
246
|
+
...fallback,
|
|
247
|
+
cause: error
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function createImportReport() {
|
|
251
|
+
return {
|
|
252
|
+
errors: [],
|
|
253
|
+
warnings: [],
|
|
254
|
+
errorCountsByCode: {},
|
|
255
|
+
warningCountsByCode: {}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function addImportError(report, error) {
|
|
259
|
+
report.errors.push(error);
|
|
260
|
+
report.errorCountsByCode[error.code] = (report.errorCountsByCode[error.code] ?? 0) + 1;
|
|
261
|
+
}
|
|
262
|
+
function addImportWarning(report, warning) {
|
|
263
|
+
report.warnings.push(warning);
|
|
264
|
+
report.warningCountsByCode[warning.code] = (report.warningCountsByCode[warning.code] ?? 0) + 1;
|
|
265
|
+
}
|
|
266
|
+
function formatGtfsError(error, options = { verbosity: "developer" }) {
|
|
267
|
+
if (!isGtfsError(error)) {
|
|
268
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
269
|
+
return options.verbosity === "user" ? message : `UNKNOWN_ERROR: ${message}`;
|
|
270
|
+
}
|
|
271
|
+
if (options.verbosity === "user") return error.message;
|
|
272
|
+
return [
|
|
273
|
+
`${error.code}: ${error.message}`,
|
|
274
|
+
`category=${error.category}`,
|
|
275
|
+
error.statusCode !== void 0 ? `statusCode=${error.statusCode}` : null,
|
|
276
|
+
error.details ? `details=${JSON.stringify(error.details)}` : null
|
|
277
|
+
].filter(Boolean).join(" | ");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/lib/db.ts
|
|
282
|
+
const dbs = {};
|
|
283
|
+
function setupDb(sqlitePath) {
|
|
284
|
+
const db = new Database$1(untildify(sqlitePath));
|
|
285
|
+
db.pragma("journal_mode = OFF");
|
|
286
|
+
db.pragma("synchronous = OFF");
|
|
287
|
+
db.pragma("temp_store = MEMORY");
|
|
288
|
+
dbs[sqlitePath] = db;
|
|
289
|
+
return db;
|
|
290
|
+
}
|
|
291
|
+
function openDb(config = null) {
|
|
292
|
+
if (config) {
|
|
293
|
+
const { sqlitePath = ":memory:", db } = config;
|
|
294
|
+
if (db) return db;
|
|
295
|
+
if (dbs[sqlitePath]) return dbs[sqlitePath];
|
|
296
|
+
return setupDb(sqlitePath);
|
|
297
|
+
}
|
|
298
|
+
if (Object.keys(dbs).length === 0) return setupDb(":memory:");
|
|
299
|
+
if (Object.keys(dbs).length === 1) return dbs[Object.keys(dbs)[0]];
|
|
300
|
+
if (Object.keys(dbs).length > 1) throw new GtfsError("Multiple databases open, please specify which one to use.", {
|
|
301
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
302
|
+
category: "database",
|
|
303
|
+
details: { openDatabaseCount: Object.keys(dbs).length }
|
|
304
|
+
});
|
|
305
|
+
throw new GtfsError("Unable to find database connection.", {
|
|
306
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
307
|
+
category: "database"
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function closeDb(db = null) {
|
|
311
|
+
if (Object.keys(dbs).length === 0) throw new GtfsError("No database connection. Call `openDb(config)` before using any methods.", {
|
|
312
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
313
|
+
category: "database"
|
|
314
|
+
});
|
|
315
|
+
if (!db) {
|
|
316
|
+
if (Object.keys(dbs).length > 1) throw new GtfsError("Multiple database connections. Pass the db you want to close as a parameter to `closeDb`.", {
|
|
317
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
318
|
+
category: "database",
|
|
319
|
+
details: { openDatabaseCount: Object.keys(dbs).length }
|
|
320
|
+
});
|
|
321
|
+
db = dbs[Object.keys(dbs)[0]];
|
|
322
|
+
}
|
|
323
|
+
db.close();
|
|
324
|
+
delete dbs[db.name];
|
|
325
|
+
}
|
|
326
|
+
function deleteDb(db = null) {
|
|
327
|
+
if (Object.keys(dbs).length === 0) throw new GtfsError("No database connection. Call `openDb(config)` before using any methods.", {
|
|
328
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
329
|
+
category: "database"
|
|
330
|
+
});
|
|
331
|
+
if (!db) {
|
|
332
|
+
if (Object.keys(dbs).length > 1) throw new GtfsError("Multiple database connections. Pass the db you want to delete as a parameter to `deleteDb`.", {
|
|
333
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
334
|
+
category: "database",
|
|
335
|
+
details: { openDatabaseCount: Object.keys(dbs).length }
|
|
336
|
+
});
|
|
337
|
+
db = dbs[Object.keys(dbs)[0]];
|
|
338
|
+
}
|
|
339
|
+
db.close();
|
|
340
|
+
if (db.name !== ":memory:") fs.unlinkSync(db.name);
|
|
341
|
+
delete dbs[db.name];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/lib/geojson-utils.ts
|
|
346
|
+
/**
|
|
347
|
+
* Validates if a string is valid JSON
|
|
348
|
+
* @param {string} string - The string to validate as JSON
|
|
349
|
+
* @returns {boolean} True if string is valid JSON, false otherwise
|
|
350
|
+
* @example
|
|
351
|
+
* isValidJSON('{"key": "value"}') // returns true
|
|
352
|
+
* isValidJSON('invalid json') // returns false
|
|
353
|
+
*/
|
|
354
|
+
function isValidJSON(string) {
|
|
355
|
+
try {
|
|
356
|
+
JSON.parse(string);
|
|
357
|
+
return true;
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Validates if an array of positions forms a valid LineString
|
|
364
|
+
* @param {Position[]} [lineString] - Array of coordinate pairs
|
|
365
|
+
* @returns {boolean} True if lineString is valid, false otherwise
|
|
366
|
+
*/
|
|
367
|
+
function isValidLineString(lineString) {
|
|
368
|
+
if (!lineString || lineString.length <= 1) return false;
|
|
369
|
+
if (lineString.length === 2) {
|
|
370
|
+
const [[x1, y1], [x2, y2]] = lineString;
|
|
371
|
+
return !(x1 === x2 && y1 === y2);
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Consolidates shape groups into unique line segments
|
|
377
|
+
* @param {Shape[][]} shapeGroups - Array of shape point groups
|
|
378
|
+
* @returns {Position[][]} Array of consolidated line strings
|
|
379
|
+
*/
|
|
380
|
+
function consolidateShapes(shapeGroups) {
|
|
381
|
+
const keys = /* @__PURE__ */ new Set();
|
|
382
|
+
const segmentsArray = shapeGroups.map((shapes) => shapes.reduce((memo, point, idx) => {
|
|
383
|
+
if (idx > 0) {
|
|
384
|
+
const prevPoint = shapes[idx - 1];
|
|
385
|
+
memo.push([[prevPoint.shape_pt_lon, prevPoint.shape_pt_lat], [point.shape_pt_lon, point.shape_pt_lat]]);
|
|
386
|
+
}
|
|
387
|
+
return memo;
|
|
388
|
+
}, []));
|
|
389
|
+
const consolidatedLineStrings = [];
|
|
390
|
+
for (const segments of segmentsArray) {
|
|
391
|
+
consolidatedLineStrings.push([]);
|
|
392
|
+
for (const segment of segments) {
|
|
393
|
+
const key1 = segment.flat().join(",");
|
|
394
|
+
const key2 = segment.reverse().flat().join(",");
|
|
395
|
+
const currentLine = last(consolidatedLineStrings);
|
|
396
|
+
if (!currentLine || keys.has(key1) || keys.has(key2)) {
|
|
397
|
+
consolidatedLineStrings.push([]);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (currentLine.length === 0) currentLine.push(segment[0]);
|
|
401
|
+
currentLine.push(segment[1]);
|
|
402
|
+
keys.add(key1);
|
|
403
|
+
keys.add(key2);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return filter(consolidatedLineStrings, isValidLineString);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Formats a color string to hex format
|
|
410
|
+
* @param {string | null | undefined} color - Color string to format
|
|
411
|
+
* @returns {string | undefined} Formatted hex color or undefined
|
|
412
|
+
* @example
|
|
413
|
+
* formatHexColor('FF0000') // returns '#FF0000'
|
|
414
|
+
*/
|
|
415
|
+
function formatHexColor(color) {
|
|
416
|
+
if (!color) return void 0;
|
|
417
|
+
return `#${color}`;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Formats properties object by cleaning null values and formatting colors
|
|
421
|
+
* @param {Record<string, unknown>} properties - Properties object to format
|
|
422
|
+
* @returns {Record<string, unknown>} Formatted properties object
|
|
423
|
+
*/
|
|
424
|
+
function formatProperties(properties) {
|
|
425
|
+
const formattedProperties = cloneDeep(omitBy(properties, (value) => value == null));
|
|
426
|
+
const formattedRouteColor = formatHexColor(properties.route_color);
|
|
427
|
+
const formattedRouteTextColor = formatHexColor(properties.route_text_color);
|
|
428
|
+
if (formattedRouteColor) formattedProperties.route_color = formattedRouteColor;
|
|
429
|
+
if (formattedRouteTextColor) formattedProperties.route_text_color = formattedRouteTextColor;
|
|
430
|
+
if (properties.routes && Array.isArray(properties.routes)) formattedProperties.routes = properties.routes.map((route) => formatProperties(route));
|
|
431
|
+
return formattedProperties;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Converts GTFS shapes to GeoJSON Feature
|
|
435
|
+
* @param {Shape[]} shapes - Array of GTFS shapes
|
|
436
|
+
* @param {Record<string, unknown>} [properties={}] - Properties to add to the feature
|
|
437
|
+
* @returns {Feature} GeoJSON Feature with MultiLineString geometry
|
|
438
|
+
*/
|
|
439
|
+
function shapesToGeoJSONFeature(shapes, properties = {}) {
|
|
440
|
+
return feature({
|
|
441
|
+
type: "MultiLineString",
|
|
442
|
+
coordinates: consolidateShapes(Object.values(groupBy(shapes, "shape_id")).map((shapeGroup) => sortBy(shapeGroup, "shape_pt_sequence")))
|
|
443
|
+
}, formatProperties(properties));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Converts GTFS stops to GeoJSON FeatureCollection
|
|
447
|
+
* @param {Stop[]} stops - Array of GTFS stops
|
|
448
|
+
* @returns {FeatureCollection} GeoJSON FeatureCollection of Point features
|
|
449
|
+
*/
|
|
450
|
+
function stopsToGeoJSONFeatureCollection(stops) {
|
|
451
|
+
return featureCollection(compact(stops.map((stop) => {
|
|
452
|
+
if (!stop.stop_lon || !stop.stop_lat) return;
|
|
453
|
+
return feature({
|
|
454
|
+
type: "Point",
|
|
455
|
+
coordinates: [stop.stop_lon, stop.stop_lat]
|
|
456
|
+
}, formatProperties(omit(stop, ["stop_lat", "stop_lon"])));
|
|
457
|
+
})));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/lib/utils.ts
|
|
462
|
+
/**
|
|
463
|
+
* Validates the configuration object for GTFS import
|
|
464
|
+
* @param config The configuration object to validate
|
|
465
|
+
* @throws Error if agencies are missing or if agency lacks both url and path
|
|
466
|
+
* @returns The validated config object
|
|
467
|
+
*/
|
|
468
|
+
function validateConfigForImport(config) {
|
|
469
|
+
if (!config.agencies || config.agencies.length === 0) throw new GtfsError("No `agencies` specified in config", {
|
|
470
|
+
code: "GTFS_CONFIG_INVALID",
|
|
471
|
+
category: "config",
|
|
472
|
+
details: { field: "agencies" }
|
|
473
|
+
});
|
|
474
|
+
for (const [index, agency] of config.agencies.entries()) if (!agency.path && !agency.url) throw new GtfsError(`No Agency \`url\` or \`path\` specified in config for agency index ${index}.`, {
|
|
475
|
+
code: "GTFS_CONFIG_INVALID",
|
|
476
|
+
category: "config",
|
|
477
|
+
details: { agencyIndex: index }
|
|
478
|
+
});
|
|
479
|
+
return config;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Initializes configuration with default values
|
|
483
|
+
* @param initialConfig The user-provided configuration
|
|
484
|
+
* @returns Merged configuration with defaults
|
|
485
|
+
*/
|
|
486
|
+
function setDefaultConfig(initialConfig) {
|
|
487
|
+
return {
|
|
488
|
+
sqlitePath: ":memory:",
|
|
489
|
+
ignoreDuplicates: false,
|
|
490
|
+
ignoreErrors: false,
|
|
491
|
+
gtfsRealtimeExpirationSeconds: 0,
|
|
492
|
+
verbose: true,
|
|
493
|
+
downloadTimeout: 3e4,
|
|
494
|
+
...initialConfig
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Converts a Long timestamp to ISO date string
|
|
499
|
+
* @param longDate Object containing high, low, and unsigned values
|
|
500
|
+
* @returns ISO formatted date string
|
|
501
|
+
*/
|
|
502
|
+
function convertLongTimeToDate(longDate) {
|
|
503
|
+
const { high, low, unsigned } = longDate;
|
|
504
|
+
return (/* @__PURE__ */ new Date(Long.fromBits(low, high, unsigned).toNumber() * 1e3)).toISOString();
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Converts time string in HH:mm:ss format to seconds since midnight
|
|
508
|
+
* @param time Time string in HH:mm:ss format
|
|
509
|
+
* @returns Number of seconds since midnight, or null if invalid format
|
|
510
|
+
*/
|
|
511
|
+
function calculateSecondsFromMidnight(time) {
|
|
512
|
+
if (!time || typeof time !== "string") return null;
|
|
513
|
+
const [hours, minutes, seconds] = time.split(":").map(Number);
|
|
514
|
+
if ([
|
|
515
|
+
hours,
|
|
516
|
+
minutes,
|
|
517
|
+
seconds
|
|
518
|
+
].some(isNaN) || minutes >= 60 || seconds >= 60) return null;
|
|
519
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Ensures time components have leading zeros (e.g., "9:5:1" -> "09:05:01")
|
|
523
|
+
* @param time Time string in HH:mm:ss format
|
|
524
|
+
* @returns Formatted time string with leading zeros, or null if invalid format
|
|
525
|
+
*/
|
|
526
|
+
function padLeadingZeros(time) {
|
|
527
|
+
const split = time.split(":").map((d) => String(Number(d)).padStart(2, "0"));
|
|
528
|
+
if (split.length !== 3) return null;
|
|
529
|
+
return split.join(":");
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Formats SQL SELECT clause from array of field names or field mapping object
|
|
533
|
+
* @param fields Array of field names or object mapping source to alias
|
|
534
|
+
* @returns Formatted SELECT clause
|
|
535
|
+
*/
|
|
536
|
+
function formatSelectClause(fields) {
|
|
537
|
+
if (Array.isArray(fields)) return `SELECT ${fields.length > 0 ? fields.map((fieldName) => sqlString.escapeId(fieldName)).join(", ") : "*"}`;
|
|
538
|
+
return `SELECT ${Object.entries(fields).map((key) => `${sqlString.escapeId(key[0])} AS ${sqlString.escapeId(key[1])}`).join(", ")}`;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Formats SQL JOIN clause from array of join configurations
|
|
542
|
+
* @param joinObject Array of join options
|
|
543
|
+
* @returns Formatted JOIN clause
|
|
544
|
+
*/
|
|
545
|
+
function formatJoinClause(joinObject) {
|
|
546
|
+
return joinObject.map((data) => `${data.type ? data.type + " JOIN" : "INNER JOIN"} ${sqlString.escapeId(data.table)} ON ${data.on}`).join(" ");
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Converts degrees to radians
|
|
550
|
+
* @param angle Angle in degrees
|
|
551
|
+
* @returns Angle in radians
|
|
552
|
+
*/
|
|
553
|
+
function degree2radian(angle) {
|
|
554
|
+
return angle * Math.PI / 180;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Converts radians to degrees
|
|
558
|
+
* @param angle Angle in radians
|
|
559
|
+
* @returns Angle in degrees
|
|
560
|
+
*/
|
|
561
|
+
function radian2degree(angle) {
|
|
562
|
+
return angle / Math.PI * 180;
|
|
563
|
+
}
|
|
564
|
+
const EARTH_RADIUS_METERS = 6371e3;
|
|
565
|
+
/**
|
|
566
|
+
* Creates SQL WHERE clause for geographic bounding box search
|
|
567
|
+
* @param latitudeDegree Center latitude in degrees
|
|
568
|
+
* @param longitudeDegree Center longitude in degrees
|
|
569
|
+
* @param boundingBoxSideMeters Size of bounding box in meters
|
|
570
|
+
* @returns SQL WHERE clause for bounding box search
|
|
571
|
+
*/
|
|
572
|
+
function formatWhereClauseBoundingBox(latitudeDegree, longitudeDegree, boundingBoxSideMeters) {
|
|
573
|
+
const lat = Number(latitudeDegree);
|
|
574
|
+
const lon = Number(longitudeDegree);
|
|
575
|
+
if (isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) throw new GtfsError("Invalid latitude or longitude values", {
|
|
576
|
+
code: "GTFS_QUERY_INVALID",
|
|
577
|
+
category: "query",
|
|
578
|
+
details: {
|
|
579
|
+
latitudeDegree,
|
|
580
|
+
longitudeDegree,
|
|
581
|
+
boundingBoxSideMeters
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
const latitudeRadian = degree2radian(lat);
|
|
585
|
+
const radiusFromLatitude = Math.cos(latitudeRadian) * EARTH_RADIUS_METERS;
|
|
586
|
+
const halfSide = boundingBoxSideMeters / 2;
|
|
587
|
+
const deltaLatitude = radian2degree(halfSide / EARTH_RADIUS_METERS);
|
|
588
|
+
const deltaLongitude = radian2degree(halfSide / radiusFromLatitude);
|
|
589
|
+
return [`stop_lat BETWEEN ${lat - deltaLatitude} AND ${lat + deltaLatitude}`, `stop_lon BETWEEN ${lon - deltaLongitude} AND ${lon + deltaLongitude}`].join(" AND ");
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Formats SQL WHERE clause for a single key-value pair
|
|
593
|
+
* @param key Column name
|
|
594
|
+
* @param value Single value, array of values, or null
|
|
595
|
+
* @returns Formatted WHERE clause condition
|
|
596
|
+
*/
|
|
597
|
+
function formatWhereClause(key, value) {
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
let whereClause = `${sqlString.escapeId(key)} IN (${value.filter((v) => v !== null).map((v) => sqlString.escape(v)).join(", ")})`;
|
|
600
|
+
if (value.includes(null)) whereClause = `(${whereClause} OR ${sqlString.escapeId(key)} IS NULL)`;
|
|
601
|
+
return whereClause;
|
|
602
|
+
}
|
|
603
|
+
if (value === null) return `${sqlString.escapeId(key)} IS NULL`;
|
|
604
|
+
return `${sqlString.escapeId(key)} = ${sqlString.escape(value)}`;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Formats complete SQL WHERE clause from query object
|
|
608
|
+
* @param query Object containing column-value pairs
|
|
609
|
+
* @returns Formatted WHERE clause or empty string if no conditions
|
|
610
|
+
*/
|
|
611
|
+
function formatWhereClauses(query) {
|
|
612
|
+
if (Object.keys(query).length === 0) return "";
|
|
613
|
+
return `WHERE ${Object.entries(query).map(([key, value]) => formatWhereClause(key, value)).join(" AND ")}`;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Formats SQL ORDER BY clause from array of sorting criteria
|
|
617
|
+
* @param orderBy Array of [column, direction] tuples
|
|
618
|
+
* @returns Formatted ORDER BY clause
|
|
619
|
+
*/
|
|
620
|
+
function formatOrderByClause(orderBy) {
|
|
621
|
+
let orderByClause = "";
|
|
622
|
+
if (orderBy.length > 0) {
|
|
623
|
+
orderByClause += "ORDER BY ";
|
|
624
|
+
orderByClause += orderBy.map(([key, value]) => {
|
|
625
|
+
const direction = value === "DESC" ? "DESC" : "ASC";
|
|
626
|
+
return `${sqlString.escapeId(key)} ${direction}`;
|
|
627
|
+
}).join(", ");
|
|
628
|
+
}
|
|
629
|
+
return orderByClause;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Gets day of week name from YYYYMMDD date number
|
|
633
|
+
* @param date Date in YYYYMMDD format
|
|
634
|
+
* @returns Lowercase day name (sunday-saturday)
|
|
635
|
+
*/
|
|
636
|
+
function getDayOfWeekFromDate(date) {
|
|
637
|
+
const DAYS_OF_WEEK = [
|
|
638
|
+
"sunday",
|
|
639
|
+
"monday",
|
|
640
|
+
"tuesday",
|
|
641
|
+
"wednesday",
|
|
642
|
+
"thursday",
|
|
643
|
+
"friday",
|
|
644
|
+
"saturday"
|
|
645
|
+
];
|
|
646
|
+
if (!Number.isInteger(date) || date.toString().length !== 8) throw new GtfsError("Date must be in YYYYMMDD format", {
|
|
647
|
+
code: "GTFS_INVALID_DATE",
|
|
648
|
+
category: "validation",
|
|
649
|
+
details: { value: date }
|
|
650
|
+
});
|
|
651
|
+
const year = Math.floor(date / 1e4);
|
|
652
|
+
const month = Math.floor(date % 1e4 / 100);
|
|
653
|
+
const day = date % 100;
|
|
654
|
+
const dateObj = new Date(year, month - 1, day);
|
|
655
|
+
if (dateObj.toString() === "Invalid Date") throw new GtfsError("Invalid date", {
|
|
656
|
+
code: "GTFS_INVALID_DATE",
|
|
657
|
+
category: "validation",
|
|
658
|
+
details: { value: date }
|
|
659
|
+
});
|
|
660
|
+
return DAYS_OF_WEEK[dateObj.getDay()];
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Formats a numeric value according to the decimal precision rules of the specified currency,
|
|
664
|
+
* without any currency symbols or separators.
|
|
665
|
+
* @param value The numeric value to format (e.g., 10.5)
|
|
666
|
+
* @param currency The ISO 4217 currency code (e.g., 'USD', 'JPY', 'EUR')
|
|
667
|
+
* @returns The formatted string with appropriate decimal places
|
|
668
|
+
* Examples:
|
|
669
|
+
* - formatCurrency(10.5, 'USD') => '10.50' // USD uses 2 decimal places
|
|
670
|
+
* - formatCurrency(10.5, 'JPY') => '10' // JPY uses 0 decimal places
|
|
671
|
+
* - formatCurrency(10.523, 'BHD') => '10.523' // BHD uses 3 decimal places
|
|
672
|
+
*/
|
|
673
|
+
function formatCurrency(value, currency) {
|
|
674
|
+
const parts = new Intl.NumberFormat(void 0, {
|
|
675
|
+
style: "currency",
|
|
676
|
+
currency
|
|
677
|
+
}).formatToParts(value);
|
|
678
|
+
const integerPart = parts.find((part) => part.type === "integer")?.value ?? "0";
|
|
679
|
+
const fractionPart = parts.find((part) => part.type === "fraction")?.value ?? "";
|
|
680
|
+
return `${integerPart}${fractionPart !== "" ? `.${fractionPart}` : ""}`;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Gets the timestamp column name for a given column name
|
|
684
|
+
* @param columnName The column name
|
|
685
|
+
* @returns The timestamp column name
|
|
686
|
+
*/
|
|
687
|
+
function getTimestampColumnName(columnName) {
|
|
688
|
+
return columnName.endsWith("time") ? `${columnName}stamp` : `${columnName}_timestamp`;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Applies a prefix to a value if the column should be prefixed and the value is not null
|
|
692
|
+
* @param value The value to prefix
|
|
693
|
+
* @param columnShouldBePrefixed Whether the column should be prefixed
|
|
694
|
+
* @param prefix The prefix to apply
|
|
695
|
+
* @returns The value with the prefix applied if the column should be prefixed and the value is not null
|
|
696
|
+
*/
|
|
697
|
+
function applyPrefixToValue(value, columnShouldBePrefixed, prefix) {
|
|
698
|
+
if (!columnShouldBePrefixed || prefix === void 0 || value === null || value === void 0) return value;
|
|
699
|
+
return `${prefix}${value}`;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Pluralizes a word based on the count
|
|
703
|
+
* @param singularWord The singular word
|
|
704
|
+
* @param pluralWord The plural word
|
|
705
|
+
* @param count The count of the word
|
|
706
|
+
* @returns The pluralized word
|
|
707
|
+
*/
|
|
708
|
+
function pluralize(singularWord, pluralWord, count) {
|
|
709
|
+
return count === 1 ? singularWord : pluralWord;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/lib/import-gtfs-realtime.ts
|
|
714
|
+
const BATCH_SIZE$1 = 1e3;
|
|
715
|
+
const MAX_RETRIES = 3;
|
|
716
|
+
const RETRY_DELAY = 1e3;
|
|
717
|
+
/**
|
|
718
|
+
* Prepares a field value for database insertion
|
|
719
|
+
*/
|
|
720
|
+
function prepareRealtimeFieldValue(entity, column, task) {
|
|
721
|
+
if (column.name === "created_timestamp") return task.currentTimestamp;
|
|
722
|
+
if (column.name === "expiration_timestamp") return task.currentTimestamp + task.gtfsRealtimeExpirationSeconds;
|
|
723
|
+
const baseValue = column.source === void 0 ? column.default : get(entity, column.source, column.default);
|
|
724
|
+
const prefixedValue = applyPrefixToValue(baseValue?.__isLong__ ? convertLongTimeToDate(baseValue) : baseValue, column.prefix, task.prefix);
|
|
725
|
+
return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Creates a prepared statement for a model
|
|
729
|
+
*/
|
|
730
|
+
function createPreparedStatement(db, model) {
|
|
731
|
+
const columns = model.schema.map((column) => column.name);
|
|
732
|
+
const placeholders = model.schema.map(() => "?").join(", ");
|
|
733
|
+
return db.prepare(`REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Processes entities in batches
|
|
737
|
+
*/
|
|
738
|
+
async function processBatch(items, batchSize, processor) {
|
|
739
|
+
let totalRecordCount = 0;
|
|
740
|
+
let totalErrorCount = 0;
|
|
741
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
742
|
+
const batch = items.slice(i, i + batchSize);
|
|
743
|
+
try {
|
|
744
|
+
const result = await processor(batch);
|
|
745
|
+
totalRecordCount += result.recordCount;
|
|
746
|
+
totalErrorCount += result.errorCount;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
749
|
+
totalErrorCount += batch.length;
|
|
750
|
+
console.error(`Batch processing error: ${errorMessage}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
recordCount: totalRecordCount,
|
|
755
|
+
errorCount: totalErrorCount
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Fetches GTFS Realtime data
|
|
760
|
+
*/
|
|
761
|
+
async function fetchGtfsRealtimeData(type, task) {
|
|
762
|
+
const urlConfig = getUrlConfig(type, task);
|
|
763
|
+
if (!urlConfig) return null;
|
|
764
|
+
task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
|
|
765
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) try {
|
|
766
|
+
const response = await fetch(urlConfig.url, {
|
|
767
|
+
method: "GET",
|
|
768
|
+
redirect: "follow",
|
|
769
|
+
headers: {
|
|
770
|
+
"User-Agent": "node-gtfs",
|
|
771
|
+
...urlConfig.headers ?? {},
|
|
772
|
+
"Accept-Encoding": "gzip"
|
|
773
|
+
},
|
|
774
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
775
|
+
});
|
|
776
|
+
if (!response.ok) throw new GtfsError(`HTTP ${response.status}: ${response.statusText}`, {
|
|
777
|
+
code: "GTFS_DOWNLOAD_HTTP",
|
|
778
|
+
category: "download",
|
|
779
|
+
statusCode: response.status,
|
|
780
|
+
details: {
|
|
781
|
+
url: urlConfig.url,
|
|
782
|
+
status: response.status,
|
|
783
|
+
statusText: response.statusText
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
const buffer = await response.arrayBuffer();
|
|
787
|
+
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(new Uint8Array(buffer));
|
|
788
|
+
return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
789
|
+
enums: String,
|
|
790
|
+
longs: String,
|
|
791
|
+
bytes: String,
|
|
792
|
+
defaults: false,
|
|
793
|
+
arrays: true,
|
|
794
|
+
objects: true,
|
|
795
|
+
oneofs: true
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
const gtfsError = toGtfsError(error, {
|
|
799
|
+
message: error instanceof Error ? error.message : String(error),
|
|
800
|
+
code: "GTFS_DOWNLOAD_FAILED",
|
|
801
|
+
category: "download",
|
|
802
|
+
details: {
|
|
803
|
+
type,
|
|
804
|
+
url: urlConfig.url
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
if (attempt === MAX_RETRIES) {
|
|
808
|
+
if (task.ignoreErrors) {
|
|
809
|
+
task.logError(`Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${gtfsError.message}`);
|
|
810
|
+
if (task.report) addImportError(task.report, gtfsError);
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
throw gtfsError;
|
|
814
|
+
}
|
|
815
|
+
task.logWarning(`Attempt ${attempt} failed for ${type}: ${gtfsError.message}`);
|
|
816
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY * attempt));
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Gets URL configuration for a specific realtime type
|
|
822
|
+
*/
|
|
823
|
+
function getUrlConfig(type, task) {
|
|
824
|
+
switch (type) {
|
|
825
|
+
case "alerts": return task.realtimeAlerts;
|
|
826
|
+
case "tripupdates": return task.realtimeTripUpdates;
|
|
827
|
+
case "vehiclepositions": return task.realtimeVehiclePositions;
|
|
828
|
+
default: return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Creates a processor for service alerts
|
|
833
|
+
*/
|
|
834
|
+
function createServiceAlertsProcessor(db, task) {
|
|
835
|
+
const alertStmt = createPreparedStatement(db, serviceAlerts);
|
|
836
|
+
const informedEntityStmt = createPreparedStatement(db, serviceAlertInformedEntities);
|
|
837
|
+
const deleteInformedEntitiesStmt = db.prepare(`DELETE FROM ${serviceAlertInformedEntities.filenameBase} WHERE alert_id = ?`);
|
|
838
|
+
return async (batch) => {
|
|
839
|
+
let recordCount = 0;
|
|
840
|
+
let errorCount = 0;
|
|
841
|
+
db.transaction(() => {
|
|
842
|
+
for (const entity of batch) try {
|
|
843
|
+
const alertId = applyPrefixToValue(entity.id, true, task.prefix);
|
|
844
|
+
deleteInformedEntitiesStmt.run(alertId);
|
|
845
|
+
const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
846
|
+
alertStmt.run(alertValues);
|
|
847
|
+
recordCount++;
|
|
848
|
+
if (entity.alert?.informedEntity?.length) for (const informedEntity of entity.alert.informedEntity) {
|
|
849
|
+
informedEntity.parent = entity;
|
|
850
|
+
const entityValues = serviceAlertInformedEntities.schema.map((column) => prepareRealtimeFieldValue(informedEntity, column, task));
|
|
851
|
+
informedEntityStmt.run(entityValues);
|
|
852
|
+
recordCount++;
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
856
|
+
errorCount++;
|
|
857
|
+
task.logWarning(`Alert processing error: ${errorMessage}`);
|
|
858
|
+
}
|
|
859
|
+
})();
|
|
860
|
+
return {
|
|
861
|
+
recordCount,
|
|
862
|
+
errorCount
|
|
863
|
+
};
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Creates a processor for trip updates
|
|
868
|
+
*/
|
|
869
|
+
function createTripUpdatesProcessor(db, task) {
|
|
870
|
+
const tripUpdateStmt = createPreparedStatement(db, tripUpdates);
|
|
871
|
+
const stopTimeStmt = createPreparedStatement(db, stopTimeUpdates);
|
|
872
|
+
const deleteStopTimesByTripStmt = db.prepare(`DELETE FROM ${stopTimeUpdates.filenameBase} WHERE trip_id = ? AND trip_start_time IS ?`);
|
|
873
|
+
return async (batch) => {
|
|
874
|
+
let recordCount = 0;
|
|
875
|
+
let errorCount = 0;
|
|
876
|
+
db.transaction(() => {
|
|
877
|
+
for (const entity of batch) try {
|
|
878
|
+
const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
879
|
+
tripUpdateStmt.run(tripUpdateValues);
|
|
880
|
+
recordCount++;
|
|
881
|
+
if (entity.tripUpdate?.stopTimeUpdate?.length) {
|
|
882
|
+
const tripId = applyPrefixToValue(entity.tripUpdate?.trip?.tripId ?? null, true, task.prefix);
|
|
883
|
+
const tripStartTime = entity.tripUpdate?.trip?.startTime ?? null;
|
|
884
|
+
if (tripId !== null) deleteStopTimesByTripStmt.run(tripId, tripStartTime);
|
|
885
|
+
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
|
|
886
|
+
stopTimeUpdate.parent = entity;
|
|
887
|
+
const stopTimeValues = stopTimeUpdates.schema.map((column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task));
|
|
888
|
+
stopTimeStmt.run(stopTimeValues);
|
|
889
|
+
recordCount++;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
} catch (error) {
|
|
893
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
894
|
+
errorCount++;
|
|
895
|
+
task.logWarning(`Trip update processing error: ${errorMessage}`);
|
|
896
|
+
}
|
|
897
|
+
})();
|
|
898
|
+
return {
|
|
899
|
+
recordCount,
|
|
900
|
+
errorCount
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Creates a processor for vehicle positions
|
|
906
|
+
*/
|
|
907
|
+
function createVehiclePositionsProcessor(db, task) {
|
|
908
|
+
const vehiclePositionStmt = createPreparedStatement(db, vehiclePositions);
|
|
909
|
+
return async (batch) => {
|
|
910
|
+
let recordCount = 0;
|
|
911
|
+
let errorCount = 0;
|
|
912
|
+
db.transaction(() => {
|
|
913
|
+
for (const entity of batch) try {
|
|
914
|
+
const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
915
|
+
vehiclePositionStmt.run(fieldValues);
|
|
916
|
+
recordCount++;
|
|
917
|
+
} catch (error) {
|
|
918
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
919
|
+
errorCount++;
|
|
920
|
+
task.logWarning(`Vehicle position processing error: ${errorMessage}`);
|
|
921
|
+
}
|
|
922
|
+
})();
|
|
923
|
+
return {
|
|
924
|
+
recordCount,
|
|
925
|
+
errorCount
|
|
926
|
+
};
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Removes expired GTFS-Realtime data
|
|
931
|
+
*/
|
|
932
|
+
function removeExpiredRealtimeData(config) {
|
|
933
|
+
const db = openDb(config);
|
|
934
|
+
log(config)(`Removing expired GTFS-Realtime data`);
|
|
935
|
+
db.transaction(() => {
|
|
936
|
+
for (const table of [
|
|
937
|
+
"vehicle_positions",
|
|
938
|
+
"trip_updates",
|
|
939
|
+
"stop_time_updates",
|
|
940
|
+
"service_alerts",
|
|
941
|
+
"service_alert_informed_entities"
|
|
942
|
+
]) db.prepare(`DELETE FROM ${table} WHERE expiration_timestamp <= strftime('%s','now')`).run();
|
|
943
|
+
})();
|
|
944
|
+
log(config)(`Removed expired GTFS-Realtime data\r`, true);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Updates GTFS Realtime data
|
|
948
|
+
*/
|
|
949
|
+
async function updateGtfsRealtimeData(task) {
|
|
950
|
+
if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) return;
|
|
951
|
+
const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all([
|
|
952
|
+
task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
|
|
953
|
+
task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
|
|
954
|
+
task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
|
|
955
|
+
]);
|
|
956
|
+
const db = openDb({ sqlitePath: task.sqlitePath });
|
|
957
|
+
const recordCounts = {
|
|
958
|
+
alerts: 0,
|
|
959
|
+
tripupdates: 0,
|
|
960
|
+
vehiclepositions: 0
|
|
961
|
+
};
|
|
962
|
+
if (alertsData?.entity?.length) recordCounts.alerts = (await processBatch(alertsData.entity, BATCH_SIZE$1, createServiceAlertsProcessor(db, task))).recordCount;
|
|
963
|
+
if (tripUpdatesData?.entity?.length) recordCounts.tripupdates = (await processBatch(tripUpdatesData.entity, BATCH_SIZE$1, createTripUpdatesProcessor(db, task))).recordCount;
|
|
964
|
+
if (vehiclePositionsData?.entity?.length) recordCounts.vehiclepositions = (await processBatch(vehiclePositionsData.entity, BATCH_SIZE$1, createVehiclePositionsProcessor(db, task))).recordCount;
|
|
965
|
+
task.log(`GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Main function to update GTFS Realtime data
|
|
969
|
+
*/
|
|
970
|
+
async function updateGtfsRealtime(initialConfig) {
|
|
971
|
+
const config = setDefaultConfig(initialConfig);
|
|
972
|
+
validateConfigForImport(config);
|
|
973
|
+
try {
|
|
974
|
+
openDb(config);
|
|
975
|
+
const agencyCount = config.agencies.length;
|
|
976
|
+
log(config)(`Starting GTFS-Realtime refresh for ${pluralize("agency", "agencies", agencyCount)} using SQLite database at ${config.sqlitePath}`);
|
|
977
|
+
removeExpiredRealtimeData(config);
|
|
978
|
+
await mapSeries(config.agencies, async (agency) => {
|
|
979
|
+
let task;
|
|
980
|
+
try {
|
|
981
|
+
task = {
|
|
982
|
+
realtimeAlerts: agency.realtimeAlerts,
|
|
983
|
+
realtimeTripUpdates: agency.realtimeTripUpdates,
|
|
984
|
+
realtimeVehiclePositions: agency.realtimeVehiclePositions,
|
|
985
|
+
downloadTimeout: config.downloadTimeout,
|
|
986
|
+
gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
|
|
987
|
+
ignoreErrors: config.ignoreErrors,
|
|
988
|
+
sqlitePath: config.sqlitePath,
|
|
989
|
+
prefix: agency.prefix,
|
|
990
|
+
currentTimestamp: Math.floor(Date.now() / 1e3),
|
|
991
|
+
log: log(config),
|
|
992
|
+
logWarning: logWarning(config),
|
|
993
|
+
logError: logError(config)
|
|
994
|
+
};
|
|
995
|
+
await updateGtfsRealtimeData(task);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
const gtfsError = toGtfsError(error, {
|
|
998
|
+
message: error instanceof Error ? error.message : String(error),
|
|
999
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
1000
|
+
category: "database",
|
|
1001
|
+
details: { sqlitePath: task?.sqlitePath ?? config.sqlitePath }
|
|
1002
|
+
});
|
|
1003
|
+
if (config.ignoreErrors) {
|
|
1004
|
+
logError(config)(formatGtfsError(gtfsError));
|
|
1005
|
+
if (task?.report) addImportError(task.report, gtfsError);
|
|
1006
|
+
} else throw gtfsError;
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
log(config)(`Completed GTFS-Realtime refresh for ${pluralize("agency", "agencies", agencyCount)}\n`);
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (error.code === "SQLITE_CANTOPEN") {
|
|
1012
|
+
const dbOpenError = new GtfsError(`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`, {
|
|
1013
|
+
code: "DB_OPEN_FAILED",
|
|
1014
|
+
category: "database",
|
|
1015
|
+
details: {
|
|
1016
|
+
sqlitePath: config.sqlitePath,
|
|
1017
|
+
dbCode: error.code
|
|
1018
|
+
},
|
|
1019
|
+
cause: error
|
|
1020
|
+
});
|
|
1021
|
+
logError(config)(dbOpenError.message);
|
|
1022
|
+
throw dbOpenError;
|
|
1023
|
+
}
|
|
1024
|
+
throw toGtfsError(error, {
|
|
1025
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1026
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
1027
|
+
category: "database"
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/lib/import-gtfs.ts
|
|
1034
|
+
function reportTaskError(task, error) {
|
|
1035
|
+
if (task.report) addImportError(task.report, error);
|
|
1036
|
+
}
|
|
1037
|
+
const getTextFiles = async (folderPath) => {
|
|
1038
|
+
return (await readdir(folderPath)).filter((filename) => filename.slice(-3) === "txt");
|
|
1039
|
+
};
|
|
1040
|
+
const downloadGtfsFiles = async (task) => {
|
|
1041
|
+
if (!task.url) throw new GtfsError("No `url` specified in config", {
|
|
1042
|
+
code: "GTFS_CONFIG_INVALID",
|
|
1043
|
+
category: "config"
|
|
1044
|
+
});
|
|
1045
|
+
task.log(`Downloading GTFS from ${task.url}`);
|
|
1046
|
+
task.path = `${task.downloadDir}/gtfs.zip`;
|
|
1047
|
+
try {
|
|
1048
|
+
const response = await fetch(task.url, {
|
|
1049
|
+
method: "GET",
|
|
1050
|
+
redirect: "follow",
|
|
1051
|
+
headers: {
|
|
1052
|
+
"User-Agent": "node-gtfs",
|
|
1053
|
+
...task.headers
|
|
1054
|
+
},
|
|
1055
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
1056
|
+
});
|
|
1057
|
+
if (!response.ok) throw new GtfsError(`Unable to download GTFS from ${task.url}. Got status ${response.status}.`, {
|
|
1058
|
+
code: "GTFS_DOWNLOAD_HTTP",
|
|
1059
|
+
category: "download",
|
|
1060
|
+
statusCode: response.status,
|
|
1061
|
+
details: {
|
|
1062
|
+
url: task.url,
|
|
1063
|
+
status: response.status,
|
|
1064
|
+
statusText: response.statusText
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
const buffer = await response.arrayBuffer();
|
|
1068
|
+
await writeFile(task.path, Buffer.from(buffer));
|
|
1069
|
+
task.log("Download successful");
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
throw toGtfsError(error, {
|
|
1072
|
+
message: `Unable to download GTFS from ${task.url}.`,
|
|
1073
|
+
code: "GTFS_DOWNLOAD_FAILED",
|
|
1074
|
+
category: "download",
|
|
1075
|
+
details: { url: task.url }
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
const extractGtfsFiles = async (task) => {
|
|
1080
|
+
if (!task.path) throw new GtfsError("No `path` specified in config", {
|
|
1081
|
+
code: "GTFS_CONFIG_INVALID",
|
|
1082
|
+
category: "config",
|
|
1083
|
+
details: { field: "path" }
|
|
1084
|
+
});
|
|
1085
|
+
const gtfsPath = untildify(task.path);
|
|
1086
|
+
task.log(`Importing static GTFS from ${task.path}\r`);
|
|
1087
|
+
if (path.extname(gtfsPath) === ".zip") try {
|
|
1088
|
+
await unzip(gtfsPath, task.downloadDir);
|
|
1089
|
+
if ((await getTextFiles(task.downloadDir)).length === 0) {
|
|
1090
|
+
const folders = (await readdir(task.downloadDir)).filter((filename) => !["__MACOSX"].includes(filename)).map((filename) => path.join(task.downloadDir, filename)).filter((source) => lstatSync(source).isDirectory());
|
|
1091
|
+
if (folders.length > 1) throw new GtfsError(`More than one subfolder found in zip file at \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`, {
|
|
1092
|
+
code: "GTFS_ZIP_INVALID",
|
|
1093
|
+
category: "zip",
|
|
1094
|
+
details: {
|
|
1095
|
+
path: task.path,
|
|
1096
|
+
folderCount: folders.length
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
else if (folders.length === 0) throw new GtfsError(`No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`, {
|
|
1100
|
+
code: "GTFS_ZIP_INVALID",
|
|
1101
|
+
category: "zip",
|
|
1102
|
+
details: { path: task.path }
|
|
1103
|
+
});
|
|
1104
|
+
const subfolderName = folders[0];
|
|
1105
|
+
const directoryTextFiles = await getTextFiles(subfolderName);
|
|
1106
|
+
if (directoryTextFiles.length === 0) throw new GtfsError(`No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`, {
|
|
1107
|
+
code: "GTFS_ZIP_INVALID",
|
|
1108
|
+
category: "zip",
|
|
1109
|
+
details: {
|
|
1110
|
+
path: task.path,
|
|
1111
|
+
subfolderName
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
await Promise.all(directoryTextFiles.map(async (fileName) => rename(path.join(subfolderName, fileName), path.join(task.downloadDir, fileName))));
|
|
1115
|
+
}
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
const wrappedError = toGtfsError(error, {
|
|
1118
|
+
message: `Unable to unzip file ${task.path}`,
|
|
1119
|
+
code: "GTFS_ZIP_INVALID",
|
|
1120
|
+
category: "zip",
|
|
1121
|
+
details: { path: task.path }
|
|
1122
|
+
});
|
|
1123
|
+
task.logError(formatGtfsError(wrappedError));
|
|
1124
|
+
throw wrappedError;
|
|
1125
|
+
}
|
|
1126
|
+
else try {
|
|
1127
|
+
await cp(gtfsPath, task.downloadDir, { recursive: true });
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
throw new GtfsError(`Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`, {
|
|
1130
|
+
code: "GTFS_DOWNLOAD_FAILED",
|
|
1131
|
+
category: "download",
|
|
1132
|
+
details: { path: gtfsPath },
|
|
1133
|
+
cause: error
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
/**
|
|
1138
|
+
* Reads agency.txt from disk after extraction to find the single agency_id for a feed.
|
|
1139
|
+
* Returns the raw agency_id string if there is exactly one agency with a non-empty
|
|
1140
|
+
* agency_id, or undefined otherwise.
|
|
1141
|
+
*/
|
|
1142
|
+
const getSingleAgencyId = (downloadDir, csvOptions, logWarning) => new Promise((resolve) => {
|
|
1143
|
+
const filepath = path.join(downloadDir, "agency.txt");
|
|
1144
|
+
if (!existsSync(filepath)) {
|
|
1145
|
+
resolve(void 0);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const rows = [];
|
|
1149
|
+
const parser = parse({
|
|
1150
|
+
columns: true,
|
|
1151
|
+
relax_quotes: true,
|
|
1152
|
+
trim: true,
|
|
1153
|
+
skip_empty_lines: true,
|
|
1154
|
+
...csvOptions
|
|
1155
|
+
});
|
|
1156
|
+
parser.on("readable", () => {
|
|
1157
|
+
let record;
|
|
1158
|
+
while (record = parser.read()) rows.push(record);
|
|
1159
|
+
});
|
|
1160
|
+
parser.on("end", () => {
|
|
1161
|
+
if (rows.length !== 1) {
|
|
1162
|
+
resolve(void 0);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
resolve(rows[0].agency_id?.trim() || void 0);
|
|
1166
|
+
});
|
|
1167
|
+
parser.on("error", (err) => {
|
|
1168
|
+
logWarning(`Unable to parse agency.txt for \`fillEmptyAgencyId\`: ${err.message}`);
|
|
1169
|
+
resolve(void 0);
|
|
1170
|
+
});
|
|
1171
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
1172
|
+
});
|
|
1173
|
+
const createGtfsTables = (db) => {
|
|
1174
|
+
for (const model of Object.values(models_exports)) {
|
|
1175
|
+
if (!model.schema) continue;
|
|
1176
|
+
const sqlColumnCreateStatements = [];
|
|
1177
|
+
for (const column of model.schema) {
|
|
1178
|
+
const checks = [];
|
|
1179
|
+
if (column.min !== void 0 && column.max !== void 0) checks.push(`${column.name} >= ${column.min} AND ${column.name} <= ${column.max}`);
|
|
1180
|
+
else if (column.min !== void 0) checks.push(`${column.name} >= ${column.min}`);
|
|
1181
|
+
else if (column.max !== void 0) checks.push(`${column.name} <= ${column.max}`);
|
|
1182
|
+
if (column.type === "integer") checks.push(`(TYPEOF(${column.name}) = 'integer' OR ${column.name} IS NULL)`);
|
|
1183
|
+
else if (column.type === "real") checks.push(`(TYPEOF(${column.name}) = 'real' OR ${column.name} IS NULL)`);
|
|
1184
|
+
const required = column.required ? "NOT NULL" : "";
|
|
1185
|
+
const columnDefault = column.default ? "DEFAULT " + column.default : "";
|
|
1186
|
+
const columnCollation = column.nocase ? "COLLATE NOCASE" : "";
|
|
1187
|
+
const checkClause = checks.length > 0 ? `CHECK(${checks.join(" AND ")})` : "";
|
|
1188
|
+
sqlColumnCreateStatements.push(`${column.name} ${column.type} ${checkClause} ${required} ${columnDefault} ${columnCollation}`);
|
|
1189
|
+
if (column.type === "time") sqlColumnCreateStatements.push(`${getTimestampColumnName(column.name)} INTEGER GENERATED ALWAYS AS (
|
|
1190
|
+
CASE
|
|
1191
|
+
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
1192
|
+
ELSE CAST(
|
|
1193
|
+
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
1194
|
+
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
1195
|
+
substr(${column.name}, -2) AS INTEGER
|
|
1196
|
+
)
|
|
1197
|
+
END
|
|
1198
|
+
) STORED`);
|
|
1199
|
+
}
|
|
1200
|
+
const primaryColumns = model.schema.filter((column) => column.primary);
|
|
1201
|
+
if (primaryColumns.length > 0) sqlColumnCreateStatements.push(`PRIMARY KEY (${primaryColumns.map(({ name }) => name).join(", ")})`);
|
|
1202
|
+
db.prepare(`DROP TABLE IF EXISTS ${model.filenameBase};`).run();
|
|
1203
|
+
db.prepare(`CREATE TABLE ${model.filenameBase} (${sqlColumnCreateStatements.join(", ")});`).run();
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
const createGtfsIndexes = (db) => {
|
|
1207
|
+
for (const model of Object.values(models_exports)) {
|
|
1208
|
+
if (!model.schema) continue;
|
|
1209
|
+
for (const column of model.schema) {
|
|
1210
|
+
if (column.index) db.prepare(`CREATE INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`).run();
|
|
1211
|
+
if (column.type === "time") {
|
|
1212
|
+
const timestampColumnName = getTimestampColumnName(column.name);
|
|
1213
|
+
db.prepare(`CREATE INDEX idx_${model.filenameBase}_${timestampColumnName} ON ${model.filenameBase} (${timestampColumnName});`).run();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
const AGENCY_ID_BACKFILL_MODELS = /* @__PURE__ */ new Set([
|
|
1219
|
+
"agency",
|
|
1220
|
+
"routes",
|
|
1221
|
+
"fare_attributes",
|
|
1222
|
+
"trip_capacity",
|
|
1223
|
+
"rider_trip",
|
|
1224
|
+
"ridership"
|
|
1225
|
+
]);
|
|
1226
|
+
function shouldBackfillAgencyId(model, formattedLine) {
|
|
1227
|
+
if (AGENCY_ID_BACKFILL_MODELS.has(model.filenameBase)) return true;
|
|
1228
|
+
if (model.filenameBase === "attributions") return formattedLine.route_id == null && formattedLine.trip_id == null;
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
const formatGtfsLine = (line, model, totalLineCount, fillEmptyAgencyId, agencyId) => {
|
|
1232
|
+
const lineNumber = totalLineCount + 1;
|
|
1233
|
+
const formattedLine = {};
|
|
1234
|
+
const filenameBase = model.filenameBase;
|
|
1235
|
+
const filenameExtension = model.filenameExtension;
|
|
1236
|
+
for (const { name, type, required } of model.schema) {
|
|
1237
|
+
let value = line[name];
|
|
1238
|
+
if (value === "" || value === void 0 || value === null) {
|
|
1239
|
+
formattedLine[name] = null;
|
|
1240
|
+
if (required) throw new GtfsError(`Missing required value in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`, {
|
|
1241
|
+
code: "GTFS_REQUIRED_FIELD_MISSING",
|
|
1242
|
+
category: "validation",
|
|
1243
|
+
details: {
|
|
1244
|
+
file: `${filenameBase}.${filenameExtension}`,
|
|
1245
|
+
line: lineNumber,
|
|
1246
|
+
column: name
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (type === "date") {
|
|
1252
|
+
value = value?.toString().replace(/-/g, "");
|
|
1253
|
+
if (value.length !== 8) throw new GtfsError(`Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`, {
|
|
1254
|
+
code: "GTFS_INVALID_DATE",
|
|
1255
|
+
category: "validation",
|
|
1256
|
+
details: {
|
|
1257
|
+
file: `${filenameBase}.${filenameExtension}`,
|
|
1258
|
+
line: lineNumber,
|
|
1259
|
+
column: name,
|
|
1260
|
+
value
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
} else if (type === "time") value = padLeadingZeros(value);
|
|
1264
|
+
if (type === "json") value = JSON.stringify(value);
|
|
1265
|
+
formattedLine[name] = value;
|
|
1266
|
+
}
|
|
1267
|
+
if (fillEmptyAgencyId && agencyId !== void 0 && formattedLine.agency_id == null && shouldBackfillAgencyId(model, formattedLine)) formattedLine.agency_id = agencyId;
|
|
1268
|
+
return formattedLine;
|
|
1269
|
+
};
|
|
1270
|
+
const BATCH_SIZE = 1e5;
|
|
1271
|
+
const importGtfsFiles = async (db, task) => {
|
|
1272
|
+
await mapSeries(Object.values(models_exports), (model) => new Promise((resolve, reject) => {
|
|
1273
|
+
let totalLineCount = 0;
|
|
1274
|
+
const filename = `${model.filenameBase}.${model.filenameExtension}`;
|
|
1275
|
+
if (task.exclude && task.exclude.includes(model.filenameBase)) {
|
|
1276
|
+
task.log(`Skipping - ${filename}\r`);
|
|
1277
|
+
resolve();
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (model.extension === "gtfs-realtime") {
|
|
1281
|
+
resolve();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const filepath = path.join(task.downloadDir, `${filename}`);
|
|
1285
|
+
if (!existsSync(filepath)) {
|
|
1286
|
+
if (!model.nonstandard) task.log(`Importing - ${filename} - No file found\r`);
|
|
1287
|
+
resolve();
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
task.log(`Importing - ${filename}\r`);
|
|
1291
|
+
const columns = model.schema;
|
|
1292
|
+
const prefixedColumns = new Set(columns.filter((column) => column.prefix).map((column) => column.name));
|
|
1293
|
+
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
|
|
1294
|
+
const insert = db.prepare(prepareStatement);
|
|
1295
|
+
const insertLines = db.transaction((lines) => {
|
|
1296
|
+
for (const [rowNumber, line] of Object.entries(lines)) try {
|
|
1297
|
+
if (task.prefix === void 0) insert.run(line);
|
|
1298
|
+
else {
|
|
1299
|
+
const prefixedLine = Object.fromEntries(Object.entries(line).map(([columnName, value]) => [columnName, applyPrefixToValue(value, prefixedColumns.has(columnName), task.prefix)]));
|
|
1300
|
+
insert.run(prefixedLine);
|
|
1301
|
+
}
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
1304
|
+
const primaryColumns = columns.filter((column) => column.primary);
|
|
1305
|
+
task.logWarning(`Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(", ")}) found in ${filename}. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`);
|
|
1306
|
+
if (task.report) addImportWarning(task.report, {
|
|
1307
|
+
code: "GTFS_DUPLICATE_PRIMARY_KEY",
|
|
1308
|
+
message: `Duplicate values for primary key found in ${filename}.`,
|
|
1309
|
+
details: {
|
|
1310
|
+
file: filename,
|
|
1311
|
+
line: Number(rowNumber) + 1,
|
|
1312
|
+
columns: primaryColumns.map((column) => column.name)
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
task.logWarning(`Check ${filename} for invalid data on line ${rowNumber + 1}.`);
|
|
1317
|
+
throw toGtfsError(error, {
|
|
1318
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1319
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
1320
|
+
category: "database",
|
|
1321
|
+
details: {
|
|
1322
|
+
file: filename,
|
|
1323
|
+
line: Number(rowNumber) + 1,
|
|
1324
|
+
sqlitePath: task.sqlitePath,
|
|
1325
|
+
dbCode: error.code
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
if (model.filenameExtension === "txt") {
|
|
1331
|
+
const parser = parse({
|
|
1332
|
+
columns: true,
|
|
1333
|
+
relax_quotes: true,
|
|
1334
|
+
trim: true,
|
|
1335
|
+
skip_empty_lines: true,
|
|
1336
|
+
...task.csvOptions
|
|
1337
|
+
});
|
|
1338
|
+
let lines = [];
|
|
1339
|
+
parser.on("readable", () => {
|
|
1340
|
+
try {
|
|
1341
|
+
let record;
|
|
1342
|
+
while (record = parser.read()) {
|
|
1343
|
+
totalLineCount += 1;
|
|
1344
|
+
lines.push(formatGtfsLine(record, model, totalLineCount, task.fillEmptyAgencyId, task.agencyId));
|
|
1345
|
+
if (lines.length >= BATCH_SIZE) {
|
|
1346
|
+
insertLines(lines);
|
|
1347
|
+
lines = [];
|
|
1348
|
+
task.log(`Importing - ${filename} - ${totalLineCount} lines imported\r`, true);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
const gtfsError = toGtfsError(error, {
|
|
1353
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1354
|
+
code: "GTFS_CSV_PARSE_FAILED",
|
|
1355
|
+
category: "parse",
|
|
1356
|
+
details: { file: filename }
|
|
1357
|
+
});
|
|
1358
|
+
if (task.ignoreErrors) {
|
|
1359
|
+
reportTaskError(task, gtfsError);
|
|
1360
|
+
task.logError(`Error processing ${filename}: ${gtfsError.message}`);
|
|
1361
|
+
resolve();
|
|
1362
|
+
} else reject(gtfsError);
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
parser.on("end", () => {
|
|
1366
|
+
try {
|
|
1367
|
+
if (lines.length > 0) try {
|
|
1368
|
+
insertLines(lines);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const gtfsError = toGtfsError(error, {
|
|
1371
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1372
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
1373
|
+
category: "database",
|
|
1374
|
+
details: {
|
|
1375
|
+
file: filename,
|
|
1376
|
+
sqlitePath: task.sqlitePath
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
if (task.ignoreErrors) {
|
|
1380
|
+
task.logError(`Error inserting data for ${filename}: ${gtfsError.message}`);
|
|
1381
|
+
reportTaskError(task, gtfsError);
|
|
1382
|
+
resolve();
|
|
1383
|
+
return;
|
|
1384
|
+
} else {
|
|
1385
|
+
reject(gtfsError);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
task.log(`Importing - ${filename} - ${totalLineCount} lines imported\r`, true);
|
|
1390
|
+
resolve();
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
const gtfsError = toGtfsError(error, {
|
|
1393
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1394
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
1395
|
+
category: "database",
|
|
1396
|
+
details: {
|
|
1397
|
+
file: filename,
|
|
1398
|
+
sqlitePath: task.sqlitePath
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
if (task.ignoreErrors) {
|
|
1402
|
+
task.logError(`Error finalizing ${filename}: ${gtfsError.message}`);
|
|
1403
|
+
reportTaskError(task, gtfsError);
|
|
1404
|
+
resolve();
|
|
1405
|
+
} else reject(gtfsError);
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
parser.on("error", (error) => {
|
|
1409
|
+
const gtfsError = toGtfsError(error, {
|
|
1410
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1411
|
+
code: "GTFS_CSV_PARSE_FAILED",
|
|
1412
|
+
category: "parse",
|
|
1413
|
+
details: { file: filename }
|
|
1414
|
+
});
|
|
1415
|
+
if (task.ignoreErrors) {
|
|
1416
|
+
task.logError(`Parser error for ${filename}: ${gtfsError.message}`);
|
|
1417
|
+
reportTaskError(task, gtfsError);
|
|
1418
|
+
resolve();
|
|
1419
|
+
} else reject(gtfsError);
|
|
1420
|
+
});
|
|
1421
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
1422
|
+
} else if (model.filenameExtension === "geojson") readFile(filepath, "utf8").then((data) => {
|
|
1423
|
+
if (isValidJSON(data) === false) if (task.ignoreErrors) {
|
|
1424
|
+
task.logError(`Invalid JSON in ${filename}`);
|
|
1425
|
+
reportTaskError(task, new GtfsError(`Invalid JSON in ${filename}`, {
|
|
1426
|
+
code: "GTFS_JSON_INVALID",
|
|
1427
|
+
category: "parse",
|
|
1428
|
+
details: { file: filename }
|
|
1429
|
+
}));
|
|
1430
|
+
resolve();
|
|
1431
|
+
return;
|
|
1432
|
+
} else {
|
|
1433
|
+
reject(new GtfsError(`Invalid JSON in ${filename}`, {
|
|
1434
|
+
code: "GTFS_JSON_INVALID",
|
|
1435
|
+
category: "parse",
|
|
1436
|
+
details: { file: filename }
|
|
1437
|
+
}));
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
totalLineCount += 1;
|
|
1441
|
+
const line = formatGtfsLine({ geojson: data }, model, totalLineCount, task.fillEmptyAgencyId, task.agencyId);
|
|
1442
|
+
try {
|
|
1443
|
+
insertLines([line]);
|
|
1444
|
+
task.log(`Importing - ${filename} - ${totalLineCount} lines imported\r`, true);
|
|
1445
|
+
resolve();
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
const gtfsError = toGtfsError(error, {
|
|
1448
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1449
|
+
code: "GTFS_DB_OPERATION_FAILED",
|
|
1450
|
+
category: "database",
|
|
1451
|
+
details: {
|
|
1452
|
+
file: filename,
|
|
1453
|
+
sqlitePath: task.sqlitePath
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
if (task.ignoreErrors) {
|
|
1457
|
+
task.logError(`Error inserting data for ${filename}: ${gtfsError.message}`);
|
|
1458
|
+
reportTaskError(task, gtfsError);
|
|
1459
|
+
resolve();
|
|
1460
|
+
} else reject(gtfsError);
|
|
1461
|
+
}
|
|
1462
|
+
}).catch((error) => {
|
|
1463
|
+
const gtfsError = toGtfsError(error, {
|
|
1464
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1465
|
+
code: "GTFS_CSV_PARSE_FAILED",
|
|
1466
|
+
category: "parse",
|
|
1467
|
+
details: { file: filename }
|
|
1468
|
+
});
|
|
1469
|
+
if (task.ignoreErrors) {
|
|
1470
|
+
task.logError(`Error reading ${filename}: ${gtfsError.message}`);
|
|
1471
|
+
reportTaskError(task, gtfsError);
|
|
1472
|
+
resolve();
|
|
1473
|
+
} else reject(gtfsError);
|
|
1474
|
+
});
|
|
1475
|
+
else if (task.ignoreErrors) {
|
|
1476
|
+
task.logError(`Unsupported file type: ${model.filenameExtension} for ${filename}`);
|
|
1477
|
+
resolve();
|
|
1478
|
+
} else reject(new GtfsError(`Unsupported file type: ${model.filenameExtension}`, {
|
|
1479
|
+
code: "GTFS_UNSUPPORTED_FILE_TYPE",
|
|
1480
|
+
category: "parse",
|
|
1481
|
+
details: {
|
|
1482
|
+
file: filename,
|
|
1483
|
+
extension: model.filenameExtension
|
|
1484
|
+
}
|
|
1485
|
+
}));
|
|
1486
|
+
}));
|
|
1487
|
+
task.log(`Static GTFS import complete`);
|
|
1488
|
+
};
|
|
1489
|
+
async function importGtfs(initialConfig) {
|
|
1490
|
+
const startTime = process.hrtime.bigint();
|
|
1491
|
+
const config = setDefaultConfig(initialConfig);
|
|
1492
|
+
validateConfigForImport(config);
|
|
1493
|
+
const report = config.includeImportReport ? createImportReport() : void 0;
|
|
1494
|
+
try {
|
|
1495
|
+
const db = openDb(config);
|
|
1496
|
+
const agencyCount = config.agencies.length;
|
|
1497
|
+
log(config)(`Starting GTFS import for ${pluralize("file", "files", agencyCount)} using SQLite database at ${config.sqlitePath}`);
|
|
1498
|
+
createGtfsTables(db);
|
|
1499
|
+
await mapSeries(config.agencies, async (agency) => {
|
|
1500
|
+
try {
|
|
1501
|
+
const tempPath = temporaryDirectory();
|
|
1502
|
+
const task = {
|
|
1503
|
+
exclude: agency.exclude,
|
|
1504
|
+
headers: agency.headers,
|
|
1505
|
+
realtimeAlerts: agency.realtimeAlerts,
|
|
1506
|
+
realtimeTripUpdates: agency.realtimeTripUpdates,
|
|
1507
|
+
realtimeVehiclePositions: agency.realtimeVehiclePositions,
|
|
1508
|
+
downloadDir: tempPath,
|
|
1509
|
+
downloadTimeout: config.downloadTimeout,
|
|
1510
|
+
gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
|
|
1511
|
+
csvOptions: config.csvOptions || {},
|
|
1512
|
+
ignoreDuplicates: config.ignoreDuplicates,
|
|
1513
|
+
ignoreErrors: config.ignoreErrors,
|
|
1514
|
+
sqlitePath: config.sqlitePath,
|
|
1515
|
+
prefix: agency.prefix,
|
|
1516
|
+
fillEmptyAgencyId: agency.fillEmptyAgencyId ?? false,
|
|
1517
|
+
agencyId: agency.agencyId,
|
|
1518
|
+
currentTimestamp: Math.floor(Date.now() / 1e3),
|
|
1519
|
+
log: log(config),
|
|
1520
|
+
logWarning: logWarning(config),
|
|
1521
|
+
logError: logError(config),
|
|
1522
|
+
report
|
|
1523
|
+
};
|
|
1524
|
+
if ("url" in agency) {
|
|
1525
|
+
Object.assign(task, { url: agency.url });
|
|
1526
|
+
await downloadGtfsFiles(task);
|
|
1527
|
+
} else Object.assign(task, { path: agency.path });
|
|
1528
|
+
await extractGtfsFiles(task);
|
|
1529
|
+
if (task.fillEmptyAgencyId) {
|
|
1530
|
+
const agencyIdFromGtfs = await getSingleAgencyId(task.downloadDir, task.csvOptions, task.logWarning);
|
|
1531
|
+
if (agencyIdFromGtfs !== void 0) {
|
|
1532
|
+
if (task.agencyId !== void 0 && task.agencyId !== agencyIdFromGtfs) task.logWarning(`\`agencyId\` "${task.agencyId}" does not match the \`agency_id\` "${agencyIdFromGtfs}" in agency.txt. Using the value from agency.txt.`);
|
|
1533
|
+
task.agencyId = agencyIdFromGtfs;
|
|
1534
|
+
} else if (task.agencyId === void 0) task.logWarning("`fillEmptyAgencyId` is set but a single `agency_id` could not be determined for this feed and no `agencyId` was provided in config. `agency_id` will not be backfilled.");
|
|
1535
|
+
}
|
|
1536
|
+
await importGtfsFiles(db, task);
|
|
1537
|
+
await updateGtfsRealtimeData(task);
|
|
1538
|
+
await rm(tempPath, { recursive: true });
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
const wrappedError = toGtfsError(error, {
|
|
1541
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1542
|
+
code: "GTFS_CSV_PARSE_FAILED",
|
|
1543
|
+
category: "parse"
|
|
1544
|
+
});
|
|
1545
|
+
if (config.ignoreErrors) {
|
|
1546
|
+
logError(config)(formatGtfsError(wrappedError));
|
|
1547
|
+
if (report) addImportError(report, wrappedError);
|
|
1548
|
+
} else throw wrappedError;
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
log(config)(`Creating DB indexes`);
|
|
1552
|
+
createGtfsIndexes(db);
|
|
1553
|
+
const endTime = process.hrtime.bigint();
|
|
1554
|
+
const elapsedSeconds = Number(endTime - startTime) / 1e9;
|
|
1555
|
+
log(config)(`Completed GTFS import in ${elapsedSeconds.toFixed(1)} seconds\n`);
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
if (error.code === "SQLITE_CANTOPEN") {
|
|
1558
|
+
const dbOpenError = new GtfsError(`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`, {
|
|
1559
|
+
code: "DB_OPEN_FAILED",
|
|
1560
|
+
category: "database",
|
|
1561
|
+
details: {
|
|
1562
|
+
sqlitePath: config.sqlitePath,
|
|
1563
|
+
dbCode: error.code
|
|
1564
|
+
},
|
|
1565
|
+
cause: error
|
|
1566
|
+
});
|
|
1567
|
+
logError(config)(dbOpenError.message);
|
|
1568
|
+
throw dbOpenError;
|
|
1569
|
+
}
|
|
1570
|
+
throw toGtfsError(error, {
|
|
1571
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1572
|
+
code: "GTFS_CSV_PARSE_FAILED",
|
|
1573
|
+
category: "parse"
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
if (report) return report;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
//#endregion
|
|
1580
|
+
//#region src/lib/export.ts
|
|
1581
|
+
const getAgencies$1 = (db, config) => {
|
|
1582
|
+
try {
|
|
1583
|
+
return db.prepare("SELECT agency_name FROM agency;").all();
|
|
1584
|
+
} catch {
|
|
1585
|
+
if (config.sqlitePath === ":memory:") throw new Error("No agencies found in SQLite. You are using an in-memory database - if running this from command line be sure to specify a value for `sqlitePath` in config.json other than \":memory:\".");
|
|
1586
|
+
throw new Error("No agencies found in SQLite. Be sure to first import data into SQLite using `gtfs-import` or `importGtfs(config);`");
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
const exportGtfs = async (initialConfig) => {
|
|
1590
|
+
const config = setDefaultConfig(initialConfig);
|
|
1591
|
+
const db = openDb(config);
|
|
1592
|
+
const agencies = getAgencies$1(db, config);
|
|
1593
|
+
const agencyCount = agencies.length;
|
|
1594
|
+
if (agencyCount === 0) throw new Error("No agencies found in SQLite. Be sure to first import data into SQLite using `gtfs-import` or `importGtfs(config);`");
|
|
1595
|
+
else if (agencyCount > 1) logWarning(config)("More than one agency is defined in config.json. Export will merge all into one GTFS file.");
|
|
1596
|
+
log(config)(`Starting GTFS export for ${pluralize("agency", "agencies", agencyCount)} using SQLite database at ${config.sqlitePath}`);
|
|
1597
|
+
const folderName = generateFolderName(agencies[0].agency_name);
|
|
1598
|
+
const defaultExportPath = path.join(process.cwd(), "gtfs-export", folderName);
|
|
1599
|
+
const exportPath = untildify(config.exportPath || defaultExportPath);
|
|
1600
|
+
await prepDirectory(exportPath);
|
|
1601
|
+
if (compact(await mapSeries(Object.values(models_exports).filter((model) => model.extension !== "gtfs-realtime"), async (model) => {
|
|
1602
|
+
const filePath = path.join(exportPath, `${model.filenameBase}.${model.filenameExtension}`);
|
|
1603
|
+
const tableName = sqlString.escapeId(model.filenameBase);
|
|
1604
|
+
const lines = db.prepare(`SELECT * FROM ${tableName};`).all();
|
|
1605
|
+
if (!lines || lines.length === 0) {
|
|
1606
|
+
if (!model.nonstandard) log(config)(`Skipping (no data) - ${model.filenameBase}.${model.filenameExtension}\r`);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (model.filenameExtension === "txt") {
|
|
1610
|
+
const excludeColumns = [];
|
|
1611
|
+
if (model.filenameBase === "routes") {
|
|
1612
|
+
const routesWithAgencyId = db.prepare("SELECT agency_id FROM routes WHERE agency_id IS NOT NULL;").all();
|
|
1613
|
+
if (!routesWithAgencyId || routesWithAgencyId.length === 0) excludeColumns.push("agency_id");
|
|
1614
|
+
} else if (model.filenameBase === "fare_attributes") for (const line of lines) line.price = formatCurrency(line.price, line.currency_type);
|
|
1615
|
+
else if (model.filenameBase === "fare_products") for (const line of lines) line.amount = formatCurrency(line.amount, line.currency);
|
|
1616
|
+
await writeFile(filePath, await stringify(lines, {
|
|
1617
|
+
columns: without(model.schema.map((column) => column.name), ...excludeColumns),
|
|
1618
|
+
header: true
|
|
1619
|
+
}));
|
|
1620
|
+
} else if (model.filenameExtension === "geojson") await writeFile(filePath, lines?.[0].geojson ?? "");
|
|
1621
|
+
else throw new Error(`Unexpected filename extension: ${model.filenameExtension}`);
|
|
1622
|
+
log(config)(`Exporting - ${model.filenameBase}.${model.filenameExtension}\r`);
|
|
1623
|
+
return `${model.filenameBase}.${model.filenameExtension}`;
|
|
1624
|
+
})).length === 0) {
|
|
1625
|
+
log(config)("No GTFS data exported. Be sure to first import data into SQLite.");
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
log(config)(`Completed GTFS export to ${exportPath}`);
|
|
1629
|
+
log(config)(`Completed GTFS export for ${pluralize("agency", "agencies", agencyCount)}\n`);
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
//#endregion
|
|
1633
|
+
//#region src/lib/advancedQuery.ts
|
|
1634
|
+
function advancedQuery(table, advancedQueryOptions) {
|
|
1635
|
+
const queryOptions = {
|
|
1636
|
+
query: {},
|
|
1637
|
+
fields: [],
|
|
1638
|
+
orderBy: [],
|
|
1639
|
+
join: [],
|
|
1640
|
+
options: {},
|
|
1641
|
+
...advancedQueryOptions
|
|
1642
|
+
};
|
|
1643
|
+
const db = queryOptions.options.db ?? openDb();
|
|
1644
|
+
const tableName = sqlString.escapeId(table);
|
|
1645
|
+
const selectClause = formatSelectClause(queryOptions.fields);
|
|
1646
|
+
const whereClause = formatWhereClauses(queryOptions.query);
|
|
1647
|
+
const joinClause = formatJoinClause(queryOptions.join);
|
|
1648
|
+
const orderByClause = formatOrderByClause(queryOptions.orderBy);
|
|
1649
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${joinClause} ${whereClause} ${orderByClause};`).all();
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
//#endregion
|
|
1653
|
+
//#region src/lib/gtfs/agencies.ts
|
|
1654
|
+
function getAgencies(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1655
|
+
const db = options.db ?? openDb();
|
|
1656
|
+
const tableName = "agency";
|
|
1657
|
+
const selectClause = formatSelectClause(fields);
|
|
1658
|
+
const whereClause = formatWhereClauses(query);
|
|
1659
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1660
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
//#endregion
|
|
1664
|
+
//#region src/lib/gtfs/areas.ts
|
|
1665
|
+
function getAreas(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1666
|
+
const db = options.db ?? openDb();
|
|
1667
|
+
const tableName = "areas";
|
|
1668
|
+
const selectClause = formatSelectClause(fields);
|
|
1669
|
+
const whereClause = formatWhereClauses(query);
|
|
1670
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1671
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
//#endregion
|
|
1675
|
+
//#region src/lib/gtfs/attributions.ts
|
|
1676
|
+
function getAttributions(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1677
|
+
const db = options.db ?? openDb();
|
|
1678
|
+
const tableName = "attributions";
|
|
1679
|
+
const selectClause = formatSelectClause(fields);
|
|
1680
|
+
const whereClause = formatWhereClauses(query);
|
|
1681
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1682
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
//#endregion
|
|
1686
|
+
//#region src/lib/gtfs/booking-rules.ts
|
|
1687
|
+
function getBookingRules(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1688
|
+
const db = options.db ?? openDb();
|
|
1689
|
+
const tableName = "booking_rules";
|
|
1690
|
+
const selectClause = formatSelectClause(fields);
|
|
1691
|
+
const whereClause = formatWhereClauses(query);
|
|
1692
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1693
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/lib/gtfs/calendar-dates.ts
|
|
1698
|
+
function getCalendarDates(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1699
|
+
const db = options.db ?? openDb();
|
|
1700
|
+
const tableName = "calendar_dates";
|
|
1701
|
+
const selectClause = formatSelectClause(fields);
|
|
1702
|
+
const whereClause = formatWhereClauses(query);
|
|
1703
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1704
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region src/lib/gtfs/calendars.ts
|
|
1709
|
+
function getCalendars(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1710
|
+
const db = options.db ?? openDb();
|
|
1711
|
+
const tableName = "calendar";
|
|
1712
|
+
const selectClause = formatSelectClause(fields);
|
|
1713
|
+
const whereClause = formatWhereClauses(query);
|
|
1714
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1715
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1716
|
+
}
|
|
1717
|
+
function getServiceIdsByDate(date, options = {}) {
|
|
1718
|
+
const db = options.db ?? openDb();
|
|
1719
|
+
if (!date) throw new GtfsError("`date` is a required query parameter", {
|
|
1720
|
+
code: "GTFS_QUERY_INVALID",
|
|
1721
|
+
category: "query",
|
|
1722
|
+
details: { field: "date" }
|
|
1723
|
+
});
|
|
1724
|
+
const dayOfWeek = getDayOfWeekFromDate(date);
|
|
1725
|
+
return db.prepare(`
|
|
1726
|
+
SELECT service_id FROM (
|
|
1727
|
+
SELECT service_id
|
|
1728
|
+
FROM calendar
|
|
1729
|
+
WHERE start_date <= ? AND end_date >= ? AND ${dayOfWeek} = 1
|
|
1730
|
+
UNION
|
|
1731
|
+
SELECT service_id
|
|
1732
|
+
FROM calendar_dates
|
|
1733
|
+
WHERE date = ? AND exception_type = 1
|
|
1734
|
+
)
|
|
1735
|
+
EXCEPT
|
|
1736
|
+
SELECT service_id
|
|
1737
|
+
FROM calendar_dates
|
|
1738
|
+
WHERE date = ? AND exception_type = 2
|
|
1739
|
+
`).all(date, date, date, date).map((record) => record.service_id);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
//#endregion
|
|
1743
|
+
//#region src/lib/gtfs/fare-attributes.ts
|
|
1744
|
+
function getFareAttributes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1745
|
+
const db = options.db ?? openDb();
|
|
1746
|
+
const tableName = "fare_attributes";
|
|
1747
|
+
const selectClause = formatSelectClause(fields);
|
|
1748
|
+
const whereClause = formatWhereClauses(query);
|
|
1749
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1750
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
//#endregion
|
|
1754
|
+
//#region src/lib/gtfs/fare-leg-rules.ts
|
|
1755
|
+
function getFareLegRules(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1756
|
+
const db = options.db ?? openDb();
|
|
1757
|
+
const tableName = "fare_leg_rules";
|
|
1758
|
+
const selectClause = formatSelectClause(fields);
|
|
1759
|
+
const whereClause = formatWhereClauses(query);
|
|
1760
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1761
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
//#endregion
|
|
1765
|
+
//#region src/lib/gtfs/fare-media.ts
|
|
1766
|
+
function getFareMedia(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1767
|
+
const db = options.db ?? openDb();
|
|
1768
|
+
const tableName = "fare_media";
|
|
1769
|
+
const selectClause = formatSelectClause(fields);
|
|
1770
|
+
const whereClause = formatWhereClauses(query);
|
|
1771
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1772
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region src/lib/gtfs/fare-products.ts
|
|
1777
|
+
function getFareProducts(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1778
|
+
const db = options.db ?? openDb();
|
|
1779
|
+
const tableName = "fare_products";
|
|
1780
|
+
const selectClause = formatSelectClause(fields);
|
|
1781
|
+
const whereClause = formatWhereClauses(query);
|
|
1782
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1783
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/lib/gtfs/fare-rules.ts
|
|
1788
|
+
function getFareRules(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1789
|
+
const db = options.db ?? openDb();
|
|
1790
|
+
const tableName = "fare_rules";
|
|
1791
|
+
const selectClause = formatSelectClause(fields);
|
|
1792
|
+
const whereClause = formatWhereClauses(query);
|
|
1793
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1794
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
//#endregion
|
|
1798
|
+
//#region src/lib/gtfs/fare-transfer-rules.ts
|
|
1799
|
+
function getFareTransferRules(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1800
|
+
const db = options.db ?? openDb();
|
|
1801
|
+
const tableName = "fare_transfer_rules";
|
|
1802
|
+
const selectClause = formatSelectClause(fields);
|
|
1803
|
+
const whereClause = formatWhereClauses(query);
|
|
1804
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1805
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region src/lib/gtfs/feed-info.ts
|
|
1810
|
+
function getFeedInfo(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1811
|
+
const db = options.db ?? openDb();
|
|
1812
|
+
const tableName = "feed_info";
|
|
1813
|
+
const selectClause = formatSelectClause(fields);
|
|
1814
|
+
const whereClause = formatWhereClauses(query);
|
|
1815
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1816
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/lib/gtfs/frequencies.ts
|
|
1821
|
+
function getFrequencies(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1822
|
+
const db = options.db ?? openDb();
|
|
1823
|
+
const tableName = "frequencies";
|
|
1824
|
+
const selectClause = formatSelectClause(fields);
|
|
1825
|
+
const whereClause = formatWhereClauses(query);
|
|
1826
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1827
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region src/lib/gtfs/levels.ts
|
|
1832
|
+
function getLevels(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1833
|
+
const db = options.db ?? openDb();
|
|
1834
|
+
const tableName = "levels";
|
|
1835
|
+
const selectClause = formatSelectClause(fields);
|
|
1836
|
+
const whereClause = formatWhereClauses(query);
|
|
1837
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1838
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
//#endregion
|
|
1842
|
+
//#region src/lib/gtfs/location-groups.ts
|
|
1843
|
+
function getLocationGroups(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1844
|
+
const db = options.db ?? openDb();
|
|
1845
|
+
const tableName = "location_groups";
|
|
1846
|
+
const selectClause = formatSelectClause(fields);
|
|
1847
|
+
const whereClause = formatWhereClauses(query);
|
|
1848
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1849
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
//#endregion
|
|
1853
|
+
//#region src/lib/gtfs/location-group-stops.ts
|
|
1854
|
+
function getLocationGroupStops(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1855
|
+
const db = options.db ?? openDb();
|
|
1856
|
+
const tableName = "location_group_stops";
|
|
1857
|
+
const selectClause = formatSelectClause(fields);
|
|
1858
|
+
const whereClause = formatWhereClauses(query);
|
|
1859
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1860
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
//#endregion
|
|
1864
|
+
//#region src/lib/gtfs/locations.ts
|
|
1865
|
+
function getLocations(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1866
|
+
const db = options.db ?? openDb();
|
|
1867
|
+
const tableName = "locations";
|
|
1868
|
+
const selectClause = formatSelectClause(fields);
|
|
1869
|
+
const whereClause = formatWhereClauses(query);
|
|
1870
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1871
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
//#endregion
|
|
1875
|
+
//#region src/lib/gtfs/networks.ts
|
|
1876
|
+
function getNetworks(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1877
|
+
const db = options.db ?? openDb();
|
|
1878
|
+
const tableName = "networks";
|
|
1879
|
+
const selectClause = formatSelectClause(fields);
|
|
1880
|
+
const whereClause = formatWhereClauses(query);
|
|
1881
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1882
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
//#endregion
|
|
1886
|
+
//#region src/lib/gtfs/pathways.ts
|
|
1887
|
+
function getPathways(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1888
|
+
const db = options.db ?? openDb();
|
|
1889
|
+
const tableName = "pathways";
|
|
1890
|
+
const selectClause = formatSelectClause(fields);
|
|
1891
|
+
const whereClause = formatWhereClauses(query);
|
|
1892
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1893
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
//#endregion
|
|
1897
|
+
//#region src/lib/gtfs/rider-categories.ts
|
|
1898
|
+
function getRiderCategories(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1899
|
+
const db = options.db ?? openDb();
|
|
1900
|
+
const tableName = "rider_categories";
|
|
1901
|
+
const selectClause = formatSelectClause(fields);
|
|
1902
|
+
const whereClause = formatWhereClauses(query);
|
|
1903
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1904
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
//#endregion
|
|
1908
|
+
//#region src/lib/gtfs/route-networks.ts
|
|
1909
|
+
function getRouteNetworks(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1910
|
+
const db = options.db ?? openDb();
|
|
1911
|
+
const tableName = "route_networks";
|
|
1912
|
+
const selectClause = formatSelectClause(fields);
|
|
1913
|
+
const whereClause = formatWhereClauses(query);
|
|
1914
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1915
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
//#endregion
|
|
1919
|
+
//#region src/lib/gtfs/routes.ts
|
|
1920
|
+
function buildStoptimeSubquery$1(query) {
|
|
1921
|
+
return `SELECT DISTINCT trip_id FROM stop_times ${formatWhereClauses(query)}`;
|
|
1922
|
+
}
|
|
1923
|
+
function buildTripSubquery$2(query) {
|
|
1924
|
+
let whereClause = "";
|
|
1925
|
+
const tripQuery = omit(query, ["stop_id"]);
|
|
1926
|
+
const stoptimeQuery = pick(query, ["stop_id"]);
|
|
1927
|
+
const whereClauses = Object.entries(tripQuery).map(([key, value]) => formatWhereClause(key, value));
|
|
1928
|
+
if (Object.values(stoptimeQuery).length > 0) whereClauses.push(`trip_id IN (${buildStoptimeSubquery$1(stoptimeQuery)})`);
|
|
1929
|
+
if (whereClauses.length > 0) whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
1930
|
+
return `SELECT DISTINCT route_id FROM trips ${whereClause}`;
|
|
1931
|
+
}
|
|
1932
|
+
function getRoutes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1933
|
+
const db = options.db ?? openDb();
|
|
1934
|
+
const tableName = "routes";
|
|
1935
|
+
const selectClause = formatSelectClause(fields);
|
|
1936
|
+
let whereClause = "";
|
|
1937
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1938
|
+
const routeQuery = omit(query, ["stop_id", "service_id"]);
|
|
1939
|
+
const tripQuery = pick(query, ["stop_id", "service_id"]);
|
|
1940
|
+
const whereClauses = Object.entries(routeQuery).map(([key, value]) => formatWhereClause(key, value));
|
|
1941
|
+
if (Object.values(tripQuery).length > 0) whereClauses.push(`route_id IN (${buildTripSubquery$2(tripQuery)})`);
|
|
1942
|
+
if (whereClauses.length > 0) whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
1943
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/lib/gtfs-plus/route-attributes.ts
|
|
1948
|
+
function getRouteAttributes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1949
|
+
const db = options.db ?? openDb();
|
|
1950
|
+
const tableName = "route_attributes";
|
|
1951
|
+
const selectClause = formatSelectClause(fields);
|
|
1952
|
+
const whereClause = formatWhereClauses(query);
|
|
1953
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1954
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
//#endregion
|
|
1958
|
+
//#region src/lib/gtfs/shapes.ts
|
|
1959
|
+
function buildTripSubquery$1(query) {
|
|
1960
|
+
return `SELECT DISTINCT shape_id FROM trips ${formatWhereClauses(query)}`;
|
|
1961
|
+
}
|
|
1962
|
+
function getShapes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
1963
|
+
const db = options.db ?? openDb();
|
|
1964
|
+
const tableName = "shapes";
|
|
1965
|
+
const selectClause = formatSelectClause(fields);
|
|
1966
|
+
let whereClause = "";
|
|
1967
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
1968
|
+
const shapeQuery = omit(query, [
|
|
1969
|
+
"route_id",
|
|
1970
|
+
"trip_id",
|
|
1971
|
+
"service_id",
|
|
1972
|
+
"direction_id"
|
|
1973
|
+
]);
|
|
1974
|
+
const tripQuery = pick(query, [
|
|
1975
|
+
"route_id",
|
|
1976
|
+
"trip_id",
|
|
1977
|
+
"service_id",
|
|
1978
|
+
"direction_id"
|
|
1979
|
+
]);
|
|
1980
|
+
const whereClauses = Object.entries(shapeQuery).map(([key, value]) => formatWhereClause(key, value));
|
|
1981
|
+
if (Object.values(tripQuery).length > 0) whereClauses.push(`shape_id IN (${buildTripSubquery$1(tripQuery)})`);
|
|
1982
|
+
if (whereClauses.length > 0) whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
1983
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
1984
|
+
}
|
|
1985
|
+
function getShapesAsGeoJSON(query = {}, options = {}) {
|
|
1986
|
+
const agencies = getAgencies({}, [], [], options);
|
|
1987
|
+
return featureCollection(compact(getRoutes(pick(query, ["route_id"]), [], [], options).map((route) => {
|
|
1988
|
+
const shapes = getShapes({
|
|
1989
|
+
route_id: route.route_id,
|
|
1990
|
+
...omit(query, "route_id")
|
|
1991
|
+
}, [], [], options);
|
|
1992
|
+
if (shapes.length === 0) return;
|
|
1993
|
+
const routeAttributes = getRouteAttributes({ route_id: route.route_id }, [], [], options);
|
|
1994
|
+
const agency = agencies.find((agency) => agency.agency_id === route.agency_id);
|
|
1995
|
+
return shapesToGeoJSONFeature(shapes, {
|
|
1996
|
+
agency_name: agency ? agency.agency_name : void 0,
|
|
1997
|
+
shape_id: query.shape_id,
|
|
1998
|
+
...route,
|
|
1999
|
+
...routeAttributes?.[0] || []
|
|
2000
|
+
});
|
|
2001
|
+
})));
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
//#endregion
|
|
2005
|
+
//#region src/lib/gtfs/stop-areas.ts
|
|
2006
|
+
function getStopAreas(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2007
|
+
const db = options.db ?? openDb();
|
|
2008
|
+
const tableName = "stop_areas";
|
|
2009
|
+
const selectClause = formatSelectClause(fields);
|
|
2010
|
+
const whereClause = formatWhereClauses(query);
|
|
2011
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2012
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region src/lib/gtfs-plus/stop-attributes.ts
|
|
2017
|
+
function getStopAttributes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2018
|
+
const db = options.db ?? openDb();
|
|
2019
|
+
const tableName = "stop_attributes";
|
|
2020
|
+
const selectClause = formatSelectClause(fields);
|
|
2021
|
+
const whereClause = formatWhereClauses(query);
|
|
2022
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2023
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
//#endregion
|
|
2027
|
+
//#region src/lib/gtfs/stops.ts
|
|
2028
|
+
function buildTripSubquery(query) {
|
|
2029
|
+
return `SELECT trip_id FROM trips ${formatWhereClauses(query)}`;
|
|
2030
|
+
}
|
|
2031
|
+
function buildStoptimeSubquery(query) {
|
|
2032
|
+
return `SELECT DISTINCT stop_id FROM stop_times WHERE trip_id IN (${buildTripSubquery(query)})`;
|
|
2033
|
+
}
|
|
2034
|
+
function getStops(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2035
|
+
const db = options.db ?? openDb();
|
|
2036
|
+
const tableName = "stops";
|
|
2037
|
+
const selectClause = formatSelectClause(fields);
|
|
2038
|
+
let whereClause = "";
|
|
2039
|
+
let orderByClause = formatOrderByClause(orderBy);
|
|
2040
|
+
const stopQueryOmitKeys = [
|
|
2041
|
+
"route_id",
|
|
2042
|
+
"trip_id",
|
|
2043
|
+
"service_id",
|
|
2044
|
+
"direction_id",
|
|
2045
|
+
"shape_id"
|
|
2046
|
+
];
|
|
2047
|
+
if (options.bounding_box_side_m !== void 0) stopQueryOmitKeys.push("stop_lat", "stop_lon");
|
|
2048
|
+
const stopQuery = omit(query, stopQueryOmitKeys);
|
|
2049
|
+
const tripQuery = pick(query, [
|
|
2050
|
+
"route_id",
|
|
2051
|
+
"trip_id",
|
|
2052
|
+
"service_id",
|
|
2053
|
+
"direction_id",
|
|
2054
|
+
"shape_id"
|
|
2055
|
+
]);
|
|
2056
|
+
const whereClauses = Object.entries(stopQuery).map(([key, value]) => formatWhereClause(key, value));
|
|
2057
|
+
if (options.bounding_box_side_m !== void 0 && query.stop_lat !== void 0 && query.stop_lon !== void 0) {
|
|
2058
|
+
whereClauses.push(formatWhereClauseBoundingBox(query.stop_lat, query.stop_lon, options.bounding_box_side_m));
|
|
2059
|
+
if (orderBy.length === 0) orderByClause = `ORDER BY (((stop_lat - ${query.stop_lat}) * (stop_lat - ${query.stop_lat})) + ((stop_lon - ${query.stop_lon}) * (stop_lon - ${query.stop_lon}))) ASC`;
|
|
2060
|
+
}
|
|
2061
|
+
if (Object.values(tripQuery).length > 0) whereClauses.push(`stop_id IN (${buildStoptimeSubquery(tripQuery)})`);
|
|
2062
|
+
if (whereClauses.length > 0) whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
2063
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2064
|
+
}
|
|
2065
|
+
function getStopsAsGeoJSON(query = {}, options = {}) {
|
|
2066
|
+
const db = options.db ?? openDb();
|
|
2067
|
+
const stops = getStops(query, [], [], options);
|
|
2068
|
+
const agencies = getAgencies({}, [], [], options);
|
|
2069
|
+
return stopsToGeoJSONFeatureCollection(stops.map((stop) => {
|
|
2070
|
+
const routes = db.prepare(`SELECT * FROM routes WHERE route_id IN (SELECT DISTINCT route_id FROM trips WHERE trip_id IN (SELECT DISTINCT trip_id FROM stop_times WHERE stop_id = ?))`).all(stop.stop_id);
|
|
2071
|
+
const stopAttributes = getStopAttributes({ stop_id: stop.stop_id });
|
|
2072
|
+
return {
|
|
2073
|
+
...stop,
|
|
2074
|
+
...stopAttributes?.[0] || [],
|
|
2075
|
+
routes: orderBy(routes, (route) => route?.route_short_name ? Number.parseInt(route.route_short_name, 10) : 0),
|
|
2076
|
+
agency_name: agencies[0]?.agency_name ?? null
|
|
2077
|
+
};
|
|
2078
|
+
}).filter((stop) => stop.routes.length > 0));
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
//#endregion
|
|
2082
|
+
//#region src/lib/gtfs/stop-times.ts
|
|
2083
|
+
function getStoptimes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2084
|
+
const db = options.db ?? openDb();
|
|
2085
|
+
const tableName = "stop_times";
|
|
2086
|
+
const selectClause = formatSelectClause(fields);
|
|
2087
|
+
let whereClause = "";
|
|
2088
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2089
|
+
const stoptimeQuery = omit(query, [
|
|
2090
|
+
"date",
|
|
2091
|
+
"start_time",
|
|
2092
|
+
"end_time"
|
|
2093
|
+
]);
|
|
2094
|
+
const whereClauses = Object.entries(stoptimeQuery).map(([key, value]) => formatWhereClause(key, value));
|
|
2095
|
+
if (query.date) {
|
|
2096
|
+
if (typeof query.date !== "number") throw new GtfsError("`date` must be a number in yyyymmdd format", {
|
|
2097
|
+
code: "GTFS_QUERY_INVALID",
|
|
2098
|
+
category: "query",
|
|
2099
|
+
details: {
|
|
2100
|
+
field: "date",
|
|
2101
|
+
value: query.date
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
const tripSubquery = `SELECT DISTINCT trip_id FROM trips WHERE service_id IN (${getServiceIdsByDate(query.date, options).map((id) => sqlString.escape(id)).join(",")})`;
|
|
2105
|
+
whereClauses.push(`trip_id IN (${tripSubquery})`);
|
|
2106
|
+
}
|
|
2107
|
+
if (query.start_time) {
|
|
2108
|
+
if (typeof query.start_time !== "string") throw new GtfsError("`start_time` must be a string in HH:mm:ss format", {
|
|
2109
|
+
code: "GTFS_QUERY_INVALID",
|
|
2110
|
+
category: "query",
|
|
2111
|
+
details: {
|
|
2112
|
+
field: "start_time",
|
|
2113
|
+
value: query.start_time
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
whereClauses.push(`arrival_timestamp >= ${calculateSecondsFromMidnight(query.start_time)}`);
|
|
2117
|
+
}
|
|
2118
|
+
if (query.end_time) {
|
|
2119
|
+
if (typeof query.end_time !== "string") throw new GtfsError("`end_time` must be a string in HH:mm:ss format", {
|
|
2120
|
+
code: "GTFS_QUERY_INVALID",
|
|
2121
|
+
category: "query",
|
|
2122
|
+
details: {
|
|
2123
|
+
field: "end_time",
|
|
2124
|
+
value: query.end_time
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
whereClauses.push(`departure_timestamp <= ${calculateSecondsFromMidnight(query.end_time)}`);
|
|
2128
|
+
}
|
|
2129
|
+
if (whereClauses.length > 0) whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
2130
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
//#endregion
|
|
2134
|
+
//#region src/lib/gtfs/timeframes.ts
|
|
2135
|
+
function getTimeframes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2136
|
+
const db = options.db ?? openDb();
|
|
2137
|
+
const tableName = "timeframes";
|
|
2138
|
+
const selectClause = formatSelectClause(fields);
|
|
2139
|
+
const whereClause = formatWhereClauses(query);
|
|
2140
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2141
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region src/lib/gtfs/transfers.ts
|
|
2146
|
+
function getTransfers(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2147
|
+
const db = options.db ?? openDb();
|
|
2148
|
+
const tableName = "transfers";
|
|
2149
|
+
const selectClause = formatSelectClause(fields);
|
|
2150
|
+
const whereClause = formatWhereClauses(query);
|
|
2151
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2152
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
//#endregion
|
|
2156
|
+
//#region src/lib/gtfs/translations.ts
|
|
2157
|
+
function getTranslations(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2158
|
+
const db = options.db ?? openDb();
|
|
2159
|
+
const tableName = "translations";
|
|
2160
|
+
const selectClause = formatSelectClause(fields);
|
|
2161
|
+
const whereClause = formatWhereClauses(query);
|
|
2162
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2163
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
//#endregion
|
|
2167
|
+
//#region src/lib/gtfs/trips.ts
|
|
2168
|
+
function getTrips(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2169
|
+
const db = options.db ?? openDb();
|
|
2170
|
+
const tableName = "trips";
|
|
2171
|
+
const selectClause = formatSelectClause(fields);
|
|
2172
|
+
let whereClause = "";
|
|
2173
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2174
|
+
const tripQuery = omit(query, ["date"]);
|
|
2175
|
+
const whereClauses = Object.entries(tripQuery).map(([key, value]) => formatWhereClause(key, value));
|
|
2176
|
+
if (query.date) {
|
|
2177
|
+
if (typeof query.date !== "number") throw new GtfsError("`date` must be a number in yyyymmdd format", {
|
|
2178
|
+
code: "GTFS_QUERY_INVALID",
|
|
2179
|
+
category: "query",
|
|
2180
|
+
details: {
|
|
2181
|
+
field: "date",
|
|
2182
|
+
value: query.date
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
const serviceIds = getServiceIdsByDate(query.date, options);
|
|
2186
|
+
whereClauses.push(`service_id IN (${serviceIds.map((id) => sqlString.escape(id)).join(",")})`);
|
|
2187
|
+
}
|
|
2188
|
+
if (whereClauses.length > 0) whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
2189
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
//#endregion
|
|
2193
|
+
//#region src/lib/gtfs-plus/calendar-attributes.ts
|
|
2194
|
+
function getCalendarAttributes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2195
|
+
const db = options.db ?? openDb();
|
|
2196
|
+
const tableName = "calendar_attributes";
|
|
2197
|
+
const selectClause = formatSelectClause(fields);
|
|
2198
|
+
const whereClause = formatWhereClauses(query);
|
|
2199
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2200
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
//#endregion
|
|
2204
|
+
//#region src/lib/gtfs-plus/directions.ts
|
|
2205
|
+
function getDirections(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2206
|
+
const db = options.db ?? openDb();
|
|
2207
|
+
const tableName = "directions";
|
|
2208
|
+
const selectClause = formatSelectClause(fields);
|
|
2209
|
+
const whereClause = formatWhereClauses(query);
|
|
2210
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2211
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
//#endregion
|
|
2215
|
+
//#region src/lib/non-standard/timetables.ts
|
|
2216
|
+
function getTimetables(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2217
|
+
const db = options.db ?? openDb();
|
|
2218
|
+
const tableName = "timetables";
|
|
2219
|
+
const selectClause = formatSelectClause(fields);
|
|
2220
|
+
const whereClause = formatWhereClauses(query);
|
|
2221
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2222
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
//#endregion
|
|
2226
|
+
//#region src/lib/non-standard/timetable-stop-order.ts
|
|
2227
|
+
function getTimetableStopOrders(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2228
|
+
const db = options.db ?? openDb();
|
|
2229
|
+
const tableName = "timetable_stop_order";
|
|
2230
|
+
const selectClause = formatSelectClause(fields);
|
|
2231
|
+
const whereClause = formatWhereClauses(query);
|
|
2232
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2233
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
//#endregion
|
|
2237
|
+
//#region src/lib/non-standard/timetable-pages.ts
|
|
2238
|
+
function getTimetablePages(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2239
|
+
const db = options.db ?? openDb();
|
|
2240
|
+
const tableName = "timetable_pages";
|
|
2241
|
+
const selectClause = formatSelectClause(fields);
|
|
2242
|
+
const whereClause = formatWhereClauses(query);
|
|
2243
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2244
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/lib/non-standard/timetable-notes.ts
|
|
2249
|
+
function getTimetableNotes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2250
|
+
const db = options.db ?? openDb();
|
|
2251
|
+
const tableName = "timetable_notes";
|
|
2252
|
+
const selectClause = formatSelectClause(fields);
|
|
2253
|
+
const whereClause = formatWhereClauses(query);
|
|
2254
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2255
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
//#endregion
|
|
2259
|
+
//#region src/lib/non-standard/timetable-notes-references.ts
|
|
2260
|
+
function getTimetableNotesReferences(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2261
|
+
const db = options.db ?? openDb();
|
|
2262
|
+
const tableName = "timetable_notes_references";
|
|
2263
|
+
const selectClause = formatSelectClause(fields);
|
|
2264
|
+
const whereClause = formatWhereClauses(query);
|
|
2265
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2266
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
//#endregion
|
|
2270
|
+
//#region src/lib/non-standard/trips-dated-vehicle-journey.ts
|
|
2271
|
+
function getTripsDatedVehicleJourneys(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2272
|
+
const db = options.db ?? openDb();
|
|
2273
|
+
const tableName = "trips_dated_vehicle_journey";
|
|
2274
|
+
const selectClause = formatSelectClause(fields);
|
|
2275
|
+
const whereClause = formatWhereClauses(query);
|
|
2276
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2277
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
//#endregion
|
|
2281
|
+
//#region src/lib/gtfs-ride/board-alights.ts
|
|
2282
|
+
function getBoardAlights(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2283
|
+
const db = options.db ?? openDb();
|
|
2284
|
+
const tableName = "board_alight";
|
|
2285
|
+
const selectClause = formatSelectClause(fields);
|
|
2286
|
+
const whereClause = formatWhereClauses(query);
|
|
2287
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2288
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
//#endregion
|
|
2292
|
+
//#region src/lib/gtfs-ride/ride-feed-info.ts
|
|
2293
|
+
function getRideFeedInfo(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2294
|
+
const db = options.db ?? openDb();
|
|
2295
|
+
const tableName = "ride_feed_info";
|
|
2296
|
+
const selectClause = formatSelectClause(fields);
|
|
2297
|
+
const whereClause = formatWhereClauses(query);
|
|
2298
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2299
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
//#endregion
|
|
2303
|
+
//#region src/lib/gtfs-ride/rider-trips.ts
|
|
2304
|
+
function getRiderTrips(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2305
|
+
const db = options.db ?? openDb();
|
|
2306
|
+
const tableName = "rider_trip";
|
|
2307
|
+
const selectClause = formatSelectClause(fields);
|
|
2308
|
+
const whereClause = formatWhereClauses(query);
|
|
2309
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2310
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
//#endregion
|
|
2314
|
+
//#region src/lib/gtfs-ride/ridership.ts
|
|
2315
|
+
function getRidership(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2316
|
+
const db = options.db ?? openDb();
|
|
2317
|
+
const tableName = "ridership";
|
|
2318
|
+
const selectClause = formatSelectClause(fields);
|
|
2319
|
+
const whereClause = formatWhereClauses(query);
|
|
2320
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2321
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
//#endregion
|
|
2325
|
+
//#region src/lib/gtfs-ride/trip-capacities.ts
|
|
2326
|
+
function getTripCapacities(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2327
|
+
const db = options.db ?? openDb();
|
|
2328
|
+
const tableName = "trip_capacity";
|
|
2329
|
+
const selectClause = formatSelectClause(fields);
|
|
2330
|
+
const whereClause = formatWhereClauses(query);
|
|
2331
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2332
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
//#endregion
|
|
2336
|
+
//#region src/lib/gtfs-realtime/stop-time-updates.ts
|
|
2337
|
+
function getStopTimeUpdates(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2338
|
+
const db = options.db ?? openDb();
|
|
2339
|
+
const tableName = "stop_time_updates";
|
|
2340
|
+
const selectClause = formatSelectClause(fields);
|
|
2341
|
+
const whereClause = formatWhereClauses(query);
|
|
2342
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2343
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
//#endregion
|
|
2347
|
+
//#region src/lib/gtfs-realtime/trip-updates.ts
|
|
2348
|
+
function getTripUpdates(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2349
|
+
const db = options.db ?? openDb();
|
|
2350
|
+
const tableName = "trip_updates";
|
|
2351
|
+
const selectClause = formatSelectClause(fields);
|
|
2352
|
+
const whereClause = formatWhereClauses(query);
|
|
2353
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2354
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
//#endregion
|
|
2358
|
+
//#region src/lib/gtfs-realtime/vehicle-positions.ts
|
|
2359
|
+
function getVehiclePositions(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2360
|
+
const db = options.db ?? openDb();
|
|
2361
|
+
const tableName = "vehicle_positions";
|
|
2362
|
+
const selectClause = formatSelectClause(fields);
|
|
2363
|
+
const whereClause = formatWhereClauses(query);
|
|
2364
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2365
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
//#endregion
|
|
2369
|
+
//#region src/lib/gtfs-realtime/service-alerts.ts
|
|
2370
|
+
const ENTITY_COLUMNS = /* @__PURE__ */ new Set([
|
|
2371
|
+
"alert_id",
|
|
2372
|
+
"stop_id",
|
|
2373
|
+
"route_id",
|
|
2374
|
+
"route_type",
|
|
2375
|
+
"trip_id",
|
|
2376
|
+
"direction_id"
|
|
2377
|
+
]);
|
|
2378
|
+
function getServiceAlerts(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2379
|
+
const db = options.db ?? openDb();
|
|
2380
|
+
const tableName = "service_alerts";
|
|
2381
|
+
const joinTableName = "service_alert_informed_entities";
|
|
2382
|
+
const selectClause = formatSelectClause(fields);
|
|
2383
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2384
|
+
const alertQuery = {};
|
|
2385
|
+
const entityQuery = {};
|
|
2386
|
+
for (const [key, value] of Object.entries(query)) if (ENTITY_COLUMNS.has(key)) entityQuery[key] = value;
|
|
2387
|
+
else alertQuery[key] = value;
|
|
2388
|
+
const whereClause = formatWhereClauses(alertQuery);
|
|
2389
|
+
const alerts = db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2390
|
+
const alertIds = alerts.map((alert) => alert.id);
|
|
2391
|
+
if (alertIds.length === 0) return [];
|
|
2392
|
+
const alertIdPlaceholders = alertIds.map(() => "?").join(", ");
|
|
2393
|
+
const entityFilterClause = formatWhereClauses(entityQuery);
|
|
2394
|
+
const entityWhereClause = entityFilterClause ? `${entityFilterClause} AND alert_id IN (${alertIdPlaceholders})` : `WHERE alert_id IN (${alertIdPlaceholders})`;
|
|
2395
|
+
const entities = db.prepare(`SELECT * FROM ${joinTableName} ${entityWhereClause};`).all(...alertIds);
|
|
2396
|
+
const entitiesByAlertId = /* @__PURE__ */ new Map();
|
|
2397
|
+
for (const entity of entities) {
|
|
2398
|
+
const group = entitiesByAlertId.get(entity.alert_id);
|
|
2399
|
+
if (group) group.push(entity);
|
|
2400
|
+
else entitiesByAlertId.set(entity.alert_id, [entity]);
|
|
2401
|
+
}
|
|
2402
|
+
return (Object.keys(entityQuery).length > 0 ? alerts.filter((alert) => entitiesByAlertId.has(alert.id)) : alerts).map((alert) => ({
|
|
2403
|
+
...alert,
|
|
2404
|
+
informed_entities: entitiesByAlertId.get(alert.id) ?? []
|
|
2405
|
+
}));
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
//#endregion
|
|
2409
|
+
//#region src/lib/gtfs-realtime/service-alert-informed-entities.ts
|
|
2410
|
+
function getServiceAlertInformedEntities(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2411
|
+
const db = options.db ?? openDb();
|
|
2412
|
+
const tableName = "service_alert_informed_entities";
|
|
2413
|
+
const selectClause = formatSelectClause(fields);
|
|
2414
|
+
const whereClause = formatWhereClauses(query);
|
|
2415
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2416
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
//#endregion
|
|
2420
|
+
//#region src/lib/ods/deadheads.ts
|
|
2421
|
+
function getDeadheads(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2422
|
+
const db = options.db ?? openDb();
|
|
2423
|
+
const tableName = "deadheads";
|
|
2424
|
+
const selectClause = formatSelectClause(fields);
|
|
2425
|
+
const whereClause = formatWhereClauses(query);
|
|
2426
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2427
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
//#endregion
|
|
2431
|
+
//#region src/lib/ods/deadhead-times.ts
|
|
2432
|
+
function getDeadheadTimes(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2433
|
+
const db = options.db ?? openDb();
|
|
2434
|
+
const tableName = "deadhead_times";
|
|
2435
|
+
const selectClause = formatSelectClause(fields);
|
|
2436
|
+
const whereClause = formatWhereClauses(query);
|
|
2437
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2438
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/lib/ods/ops-locations.ts
|
|
2443
|
+
function getOpsLocations(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2444
|
+
const db = options.db ?? openDb();
|
|
2445
|
+
const tableName = "ops_locations";
|
|
2446
|
+
const selectClause = formatSelectClause(fields);
|
|
2447
|
+
const whereClause = formatWhereClauses(query);
|
|
2448
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2449
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
//#endregion
|
|
2453
|
+
//#region src/lib/ods/run-events.ts
|
|
2454
|
+
function getRunEvents(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2455
|
+
const db = options.db ?? openDb();
|
|
2456
|
+
const tableName = "run_events";
|
|
2457
|
+
const selectClause = formatSelectClause(fields);
|
|
2458
|
+
const whereClause = formatWhereClauses(query);
|
|
2459
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2460
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
//#endregion
|
|
2464
|
+
//#region src/lib/ods/runs-pieces.ts
|
|
2465
|
+
function getRunsPieces(query = {}, fields = [], orderBy = [], options = {}) {
|
|
2466
|
+
const db = options.db ?? openDb();
|
|
2467
|
+
const tableName = "runs_pieces";
|
|
2468
|
+
const selectClause = formatSelectClause(fields);
|
|
2469
|
+
const whereClause = formatWhereClauses(query);
|
|
2470
|
+
const orderByClause = formatOrderByClause(orderBy);
|
|
2471
|
+
return db.prepare(`${selectClause} FROM ${tableName} ${whereClause} ${orderByClause};`).all();
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
//#endregion
|
|
2475
|
+
export { getCalendars as $, getStopsAsGeoJSON as A, getNetworks as B, getCalendarAttributes as C, untildify as Ct, getTimeframes as D, getTransfers as E, getRouteAttributes as F, getFrequencies as G, getLocationGroupStops as H, getRoutes as I, getFareRules as J, getFeedInfo as K, getRouteNetworks as L, getStopAreas as M, getShapes as N, getStoptimes as O, getShapesAsGeoJSON as P, getFareAttributes as Q, getRiderCategories as R, getDirections as S, prepDirectory as St, getTranslations as T, formatError as Tt, getLocationGroups as U, getLocations as V, getLevels as W, getFareMedia as X, getFareProducts as Y, getFareLegRules as Z, getTimetableNotesReferences as _, formatGtfsError as _t, getDeadheads as a, getAgencies as at, getTimetableStopOrders as b, generateFolderName as bt, getVehiclePositions as c, importGtfs as ct, getTripCapacities as d, deleteDb as dt, getServiceIdsByDate as et, getRidership as f, openDb as ft, getTripsDatedVehicleJourneys as g, GtfsWarningCode as gt, getBoardAlights as h, GtfsErrorCode as ht, getDeadheadTimes as i, getAreas as it, getStopAttributes as j, getStops as k, getTripUpdates as l, updateGtfsRealtime as lt, getRideFeedInfo as m, GtfsErrorCategory as mt, getRunEvents as n, getBookingRules as nt, getServiceAlertInformedEntities as o, advancedQuery as ot, getRiderTrips as p, GtfsError as pt, getFareTransferRules as q, getOpsLocations as r, getAttributions as rt, getServiceAlerts as s, exportGtfs as st, getRunsPieces as t, getCalendarDates as tt, getStopTimeUpdates as u, closeDb as ut, getTimetableNotes as v, isGtfsError as vt, getTrips as w, unzip as wt, getTimetables as x, getConfig as xt, getTimetablePages as y, isGtfsValidationError as yt, getPathways as z };
|
|
2476
|
+
//# sourceMappingURL=src-CdVKeHzG.js.map
|