gtfs 4.17.6 → 4.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -11
- package/dist/bin/gtfs-export.js +25 -18
- package/dist/bin/gtfs-export.js.map +1 -1
- package/dist/bin/gtfs-import.js +443 -316
- package/dist/bin/gtfs-import.js.map +1 -1
- package/dist/bin/gtfsrealtime-update.js +253 -196
- package/dist/bin/gtfsrealtime-update.js.map +1 -1
- package/dist/index.d.ts +145 -7
- package/dist/index.js +568 -427
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
package/dist/index.js
CHANGED
|
@@ -9,11 +9,8 @@ import path2 from "path";
|
|
|
9
9
|
import { createReadStream, existsSync as existsSync2, lstatSync } from "fs";
|
|
10
10
|
import { cp, readdir, rename, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
|
|
11
11
|
import { parse } from "csv-parse";
|
|
12
|
-
import pluralize2 from "pluralize";
|
|
13
12
|
import stripBomStream from "strip-bom-stream";
|
|
14
13
|
import { temporaryDirectory } from "tempy";
|
|
15
|
-
import Timer from "timer-machine";
|
|
16
|
-
import untildify3 from "untildify";
|
|
17
14
|
import mapSeries2 from "promise-map-series";
|
|
18
15
|
|
|
19
16
|
// src/models/models.ts
|
|
@@ -3976,84 +3973,14 @@ var vehicles = {
|
|
|
3976
3973
|
// src/lib/db.ts
|
|
3977
3974
|
import fs from "fs";
|
|
3978
3975
|
import Database from "better-sqlite3";
|
|
3979
|
-
import untildify from "untildify";
|
|
3980
|
-
var dbs = {};
|
|
3981
|
-
function setupDb(sqlitePath) {
|
|
3982
|
-
const db = new Database(untildify(sqlitePath));
|
|
3983
|
-
db.pragma("journal_mode = OFF");
|
|
3984
|
-
db.pragma("synchronous = OFF");
|
|
3985
|
-
db.pragma("temp_store = MEMORY");
|
|
3986
|
-
dbs[sqlitePath] = db;
|
|
3987
|
-
return db;
|
|
3988
|
-
}
|
|
3989
|
-
function openDb(config = null) {
|
|
3990
|
-
if (config) {
|
|
3991
|
-
const { sqlitePath = ":memory:", db } = config;
|
|
3992
|
-
if (db) {
|
|
3993
|
-
return db;
|
|
3994
|
-
}
|
|
3995
|
-
if (dbs[sqlitePath]) {
|
|
3996
|
-
return dbs[sqlitePath];
|
|
3997
|
-
}
|
|
3998
|
-
return setupDb(sqlitePath);
|
|
3999
|
-
}
|
|
4000
|
-
if (Object.keys(dbs).length === 0) {
|
|
4001
|
-
return setupDb(":memory:");
|
|
4002
|
-
}
|
|
4003
|
-
if (Object.keys(dbs).length === 1) {
|
|
4004
|
-
const filename = Object.keys(dbs)[0];
|
|
4005
|
-
return dbs[filename];
|
|
4006
|
-
}
|
|
4007
|
-
if (Object.keys(dbs).length > 1) {
|
|
4008
|
-
throw new Error(
|
|
4009
|
-
"Multiple databases open, please specify which one to use."
|
|
4010
|
-
);
|
|
4011
|
-
}
|
|
4012
|
-
throw new Error("Unable to find database connection.");
|
|
4013
|
-
}
|
|
4014
|
-
function closeDb(db = null) {
|
|
4015
|
-
if (Object.keys(dbs).length === 0) {
|
|
4016
|
-
throw new Error(
|
|
4017
|
-
"No database connection. Call `openDb(config)` before using any methods."
|
|
4018
|
-
);
|
|
4019
|
-
}
|
|
4020
|
-
if (!db) {
|
|
4021
|
-
if (Object.keys(dbs).length > 1) {
|
|
4022
|
-
throw new Error(
|
|
4023
|
-
"Multiple database connections. Pass the db you want to close as a parameter to `closeDb`."
|
|
4024
|
-
);
|
|
4025
|
-
}
|
|
4026
|
-
db = dbs[Object.keys(dbs)[0]];
|
|
4027
|
-
}
|
|
4028
|
-
db.close();
|
|
4029
|
-
delete dbs[db.name];
|
|
4030
|
-
}
|
|
4031
|
-
function deleteDb(db = null) {
|
|
4032
|
-
if (Object.keys(dbs).length === 0) {
|
|
4033
|
-
throw new Error(
|
|
4034
|
-
"No database connection. Call `openDb(config)` before using any methods."
|
|
4035
|
-
);
|
|
4036
|
-
}
|
|
4037
|
-
if (!db) {
|
|
4038
|
-
if (Object.keys(dbs).length > 1) {
|
|
4039
|
-
throw new Error(
|
|
4040
|
-
"Multiple database connections. Pass the db you want to delete as a parameter to `deleteDb`."
|
|
4041
|
-
);
|
|
4042
|
-
}
|
|
4043
|
-
db = dbs[Object.keys(dbs)[0]];
|
|
4044
|
-
}
|
|
4045
|
-
db.close();
|
|
4046
|
-
fs.unlinkSync(db.name);
|
|
4047
|
-
delete dbs[db.name];
|
|
4048
|
-
}
|
|
4049
3976
|
|
|
4050
3977
|
// src/lib/file-utils.ts
|
|
4051
3978
|
import path from "path";
|
|
4052
3979
|
import { existsSync } from "fs";
|
|
3980
|
+
import { homedir } from "os";
|
|
4053
3981
|
import { mkdir, readFile, rm } from "fs/promises";
|
|
4054
3982
|
import { omit, snakeCase } from "lodash-es";
|
|
4055
3983
|
import sanitize from "sanitize-filename";
|
|
4056
|
-
import untildify2 from "untildify";
|
|
4057
3984
|
import StreamZip from "node-stream-zip";
|
|
4058
3985
|
|
|
4059
3986
|
// src/lib/log-utils.ts
|
|
@@ -4107,6 +4034,7 @@ function formatError(error) {
|
|
|
4107
4034
|
}
|
|
4108
4035
|
|
|
4109
4036
|
// src/lib/file-utils.ts
|
|
4037
|
+
var homeDirectory = homedir();
|
|
4110
4038
|
async function prepDirectory(exportPath) {
|
|
4111
4039
|
await rm(exportPath, { recursive: true, force: true });
|
|
4112
4040
|
await mkdir(exportPath, { recursive: true });
|
|
@@ -4128,6 +4056,80 @@ function generateFolderName(folderName) {
|
|
|
4128
4056
|
}
|
|
4129
4057
|
return snakeCase(sanitize(folderName));
|
|
4130
4058
|
}
|
|
4059
|
+
function untildify(pathWithTilde) {
|
|
4060
|
+
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
// src/lib/db.ts
|
|
4064
|
+
var dbs = {};
|
|
4065
|
+
function setupDb(sqlitePath) {
|
|
4066
|
+
const db = new Database(untildify(sqlitePath));
|
|
4067
|
+
db.pragma("journal_mode = OFF");
|
|
4068
|
+
db.pragma("synchronous = OFF");
|
|
4069
|
+
db.pragma("temp_store = MEMORY");
|
|
4070
|
+
dbs[sqlitePath] = db;
|
|
4071
|
+
return db;
|
|
4072
|
+
}
|
|
4073
|
+
function openDb(config = null) {
|
|
4074
|
+
if (config) {
|
|
4075
|
+
const { sqlitePath = ":memory:", db } = config;
|
|
4076
|
+
if (db) {
|
|
4077
|
+
return db;
|
|
4078
|
+
}
|
|
4079
|
+
if (dbs[sqlitePath]) {
|
|
4080
|
+
return dbs[sqlitePath];
|
|
4081
|
+
}
|
|
4082
|
+
return setupDb(sqlitePath);
|
|
4083
|
+
}
|
|
4084
|
+
if (Object.keys(dbs).length === 0) {
|
|
4085
|
+
return setupDb(":memory:");
|
|
4086
|
+
}
|
|
4087
|
+
if (Object.keys(dbs).length === 1) {
|
|
4088
|
+
const filename = Object.keys(dbs)[0];
|
|
4089
|
+
return dbs[filename];
|
|
4090
|
+
}
|
|
4091
|
+
if (Object.keys(dbs).length > 1) {
|
|
4092
|
+
throw new Error(
|
|
4093
|
+
"Multiple databases open, please specify which one to use."
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
throw new Error("Unable to find database connection.");
|
|
4097
|
+
}
|
|
4098
|
+
function closeDb(db = null) {
|
|
4099
|
+
if (Object.keys(dbs).length === 0) {
|
|
4100
|
+
throw new Error(
|
|
4101
|
+
"No database connection. Call `openDb(config)` before using any methods."
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
if (!db) {
|
|
4105
|
+
if (Object.keys(dbs).length > 1) {
|
|
4106
|
+
throw new Error(
|
|
4107
|
+
"Multiple database connections. Pass the db you want to close as a parameter to `closeDb`."
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
db = dbs[Object.keys(dbs)[0]];
|
|
4111
|
+
}
|
|
4112
|
+
db.close();
|
|
4113
|
+
delete dbs[db.name];
|
|
4114
|
+
}
|
|
4115
|
+
function deleteDb(db = null) {
|
|
4116
|
+
if (Object.keys(dbs).length === 0) {
|
|
4117
|
+
throw new Error(
|
|
4118
|
+
"No database connection. Call `openDb(config)` before using any methods."
|
|
4119
|
+
);
|
|
4120
|
+
}
|
|
4121
|
+
if (!db) {
|
|
4122
|
+
if (Object.keys(dbs).length > 1) {
|
|
4123
|
+
throw new Error(
|
|
4124
|
+
"Multiple database connections. Pass the db you want to delete as a parameter to `deleteDb`."
|
|
4125
|
+
);
|
|
4126
|
+
}
|
|
4127
|
+
db = dbs[Object.keys(dbs)[0]];
|
|
4128
|
+
}
|
|
4129
|
+
db.close();
|
|
4130
|
+
fs.unlinkSync(db.name);
|
|
4131
|
+
delete dbs[db.name];
|
|
4132
|
+
}
|
|
4131
4133
|
|
|
4132
4134
|
// src/lib/geojson-utils.ts
|
|
4133
4135
|
import {
|
|
@@ -4145,7 +4147,7 @@ function isValidJSON(string) {
|
|
|
4145
4147
|
try {
|
|
4146
4148
|
JSON.parse(string);
|
|
4147
4149
|
return true;
|
|
4148
|
-
} catch
|
|
4150
|
+
} catch {
|
|
4149
4151
|
return false;
|
|
4150
4152
|
}
|
|
4151
4153
|
}
|
|
@@ -4210,7 +4212,7 @@ function formatProperties(properties) {
|
|
|
4210
4212
|
if (formattedRouteTextColor) {
|
|
4211
4213
|
formattedProperties.route_text_color = formattedRouteTextColor;
|
|
4212
4214
|
}
|
|
4213
|
-
if (properties.routes) {
|
|
4215
|
+
if (properties.routes && Array.isArray(properties.routes)) {
|
|
4214
4216
|
formattedProperties.routes = properties.routes.map(
|
|
4215
4217
|
(route) => formatProperties(route)
|
|
4216
4218
|
);
|
|
@@ -4249,7 +4251,6 @@ function stopsToGeoJSONFeatureCollection(stops2) {
|
|
|
4249
4251
|
}
|
|
4250
4252
|
|
|
4251
4253
|
// src/lib/import-gtfs-realtime.ts
|
|
4252
|
-
import pluralize from "pluralize";
|
|
4253
4254
|
import GtfsRealtimeBindings from "gtfs-realtime-bindings";
|
|
4254
4255
|
import mapSeries from "promise-map-series";
|
|
4255
4256
|
import { get } from "lodash-es";
|
|
@@ -4276,7 +4277,8 @@ function setDefaultConfig(initialConfig) {
|
|
|
4276
4277
|
ignoreDuplicates: false,
|
|
4277
4278
|
ignoreErrors: false,
|
|
4278
4279
|
gtfsRealtimeExpirationSeconds: 0,
|
|
4279
|
-
verbose: true
|
|
4280
|
+
verbose: true,
|
|
4281
|
+
downloadTimeout: 3e4
|
|
4280
4282
|
};
|
|
4281
4283
|
return {
|
|
4282
4284
|
...defaults,
|
|
@@ -4419,58 +4421,14 @@ function applyPrefixToValue(value, columnShouldBePrefixed, prefix) {
|
|
|
4419
4421
|
}
|
|
4420
4422
|
return `${prefix}${value}`;
|
|
4421
4423
|
}
|
|
4424
|
+
function pluralize(singularWord, pluralWord, count) {
|
|
4425
|
+
return count === 1 ? singularWord : pluralWord;
|
|
4426
|
+
}
|
|
4422
4427
|
|
|
4423
4428
|
// src/lib/import-gtfs-realtime.ts
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
method: "GET",
|
|
4428
|
-
headers: {
|
|
4429
|
-
...urlConfig.headers ?? {},
|
|
4430
|
-
"Accept-Encoding": "gzip"
|
|
4431
|
-
},
|
|
4432
|
-
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
4433
|
-
});
|
|
4434
|
-
if (response.status !== 200) {
|
|
4435
|
-
task.logWarning(
|
|
4436
|
-
`Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
|
|
4437
|
-
);
|
|
4438
|
-
return null;
|
|
4439
|
-
}
|
|
4440
|
-
const buffer = await response.arrayBuffer();
|
|
4441
|
-
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
4442
|
-
new Uint8Array(buffer)
|
|
4443
|
-
);
|
|
4444
|
-
return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
4445
|
-
enums: String,
|
|
4446
|
-
longs: String,
|
|
4447
|
-
bytes: String,
|
|
4448
|
-
defaults: false,
|
|
4449
|
-
arrays: true,
|
|
4450
|
-
objects: true,
|
|
4451
|
-
oneofs: true
|
|
4452
|
-
});
|
|
4453
|
-
}
|
|
4454
|
-
function removeExpiredRealtimeData(config) {
|
|
4455
|
-
const db = openDb(config);
|
|
4456
|
-
log(config)(`Removing expired GTFS-Realtime data`);
|
|
4457
|
-
db.prepare(
|
|
4458
|
-
`DELETE FROM vehicle_positions WHERE expiration_timestamp <= strftime('%s','now')`
|
|
4459
|
-
).run();
|
|
4460
|
-
db.prepare(
|
|
4461
|
-
`DELETE FROM trip_updates WHERE expiration_timestamp <= strftime('%s','now')`
|
|
4462
|
-
).run();
|
|
4463
|
-
db.prepare(
|
|
4464
|
-
`DELETE FROM stop_time_updates WHERE expiration_timestamp <= strftime('%s','now')`
|
|
4465
|
-
).run();
|
|
4466
|
-
db.prepare(
|
|
4467
|
-
`DELETE FROM service_alerts WHERE expiration_timestamp <= strftime('%s','now')`
|
|
4468
|
-
).run();
|
|
4469
|
-
db.prepare(
|
|
4470
|
-
`DELETE FROM service_alert_informed_entities WHERE expiration_timestamp <= strftime('%s','now')`
|
|
4471
|
-
).run();
|
|
4472
|
-
log(config)(`Removed expired GTFS-Realtime data\r`, true);
|
|
4473
|
-
}
|
|
4429
|
+
var BATCH_SIZE = 1e3;
|
|
4430
|
+
var MAX_RETRIES = 3;
|
|
4431
|
+
var RETRY_DELAY = 1e3;
|
|
4474
4432
|
function prepareRealtimeFieldValue(entity, column, task) {
|
|
4475
4433
|
if (column.name === "created_timestamp") {
|
|
4476
4434
|
return task.currentTimestamp;
|
|
@@ -4487,163 +4445,265 @@ function prepareRealtimeFieldValue(entity, column, task) {
|
|
|
4487
4445
|
);
|
|
4488
4446
|
return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
|
|
4489
4447
|
}
|
|
4490
|
-
|
|
4491
|
-
const
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
);
|
|
4496
|
-
const informedEntityStmt = db.prepare(
|
|
4497
|
-
`REPLACE INTO ${serviceAlertInformedEntities.filenameBase} (${serviceAlertInformedEntities.schema.map((column) => column.name).join(
|
|
4498
|
-
", "
|
|
4499
|
-
)}) VALUES (${serviceAlertInformedEntities.schema.map(() => "?").join(", ")})`
|
|
4448
|
+
function createPreparedStatement(db, model) {
|
|
4449
|
+
const columns = model.schema.map((column) => column.name);
|
|
4450
|
+
const placeholders = model.schema.map(() => "?").join(", ");
|
|
4451
|
+
return db.prepare(
|
|
4452
|
+
`REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
|
|
4500
4453
|
);
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4454
|
+
}
|
|
4455
|
+
async function processBatch(items, batchSize, processor) {
|
|
4456
|
+
let totalRecordCount = 0;
|
|
4457
|
+
let totalErrorCount = 0;
|
|
4458
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
4459
|
+
const batch = items.slice(i, i + batchSize);
|
|
4460
|
+
try {
|
|
4461
|
+
const result = await processor(batch);
|
|
4462
|
+
totalRecordCount += result.recordCount;
|
|
4463
|
+
totalErrorCount += result.errorCount;
|
|
4464
|
+
} catch (error) {
|
|
4465
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4466
|
+
totalErrorCount += batch.length;
|
|
4467
|
+
console.error(`Batch processing error: ${errorMessage}`);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
return { recordCount: totalRecordCount, errorCount: totalErrorCount };
|
|
4471
|
+
}
|
|
4472
|
+
async function fetchGtfsRealtimeData(type, task) {
|
|
4473
|
+
const urlConfig = getUrlConfig(type, task);
|
|
4474
|
+
if (!urlConfig) {
|
|
4475
|
+
return null;
|
|
4476
|
+
}
|
|
4477
|
+
task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
|
|
4478
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
4479
|
+
try {
|
|
4480
|
+
const response = await fetch(urlConfig.url, {
|
|
4481
|
+
method: "GET",
|
|
4482
|
+
headers: {
|
|
4483
|
+
...urlConfig.headers ?? {},
|
|
4484
|
+
"Accept-Encoding": "gzip"
|
|
4485
|
+
},
|
|
4486
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
4487
|
+
});
|
|
4488
|
+
if (response.status !== 200) {
|
|
4489
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4490
|
+
}
|
|
4491
|
+
const buffer = await response.arrayBuffer();
|
|
4492
|
+
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
4493
|
+
new Uint8Array(buffer)
|
|
4506
4494
|
);
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4495
|
+
const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
4496
|
+
enums: String,
|
|
4497
|
+
longs: String,
|
|
4498
|
+
bytes: String,
|
|
4499
|
+
defaults: false,
|
|
4500
|
+
arrays: true,
|
|
4501
|
+
objects: true,
|
|
4502
|
+
oneofs: true
|
|
4503
|
+
});
|
|
4504
|
+
return feedMessage;
|
|
4505
|
+
} catch (error) {
|
|
4506
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4507
|
+
if (attempt === MAX_RETRIES) {
|
|
4508
|
+
if (task.ignoreErrors) {
|
|
4509
|
+
task.logError(
|
|
4510
|
+
`Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
|
|
4511
|
+
);
|
|
4512
|
+
return null;
|
|
4513
|
+
}
|
|
4514
|
+
throw error;
|
|
4515
|
+
}
|
|
4516
|
+
task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
|
|
4517
|
+
await new Promise(
|
|
4518
|
+
(resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
|
|
4519
|
+
);
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
return null;
|
|
4523
|
+
}
|
|
4524
|
+
function getUrlConfig(type, task) {
|
|
4525
|
+
switch (type) {
|
|
4526
|
+
case "alerts":
|
|
4527
|
+
return task.realtimeAlerts;
|
|
4528
|
+
case "tripupdates":
|
|
4529
|
+
return task.realtimeTripUpdates;
|
|
4530
|
+
case "vehiclepositions":
|
|
4531
|
+
return task.realtimeVehiclePositions;
|
|
4532
|
+
default:
|
|
4533
|
+
return void 0;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
function createServiceAlertsProcessor(db, task) {
|
|
4537
|
+
const alertStmt = createPreparedStatement(db, serviceAlerts);
|
|
4538
|
+
const informedEntityStmt = createPreparedStatement(
|
|
4539
|
+
db,
|
|
4540
|
+
serviceAlertInformedEntities
|
|
4541
|
+
);
|
|
4542
|
+
return async (batch) => {
|
|
4543
|
+
let recordCount = 0;
|
|
4544
|
+
let errorCount = 0;
|
|
4545
|
+
db.transaction(() => {
|
|
4546
|
+
for (const entity of batch) {
|
|
4547
|
+
try {
|
|
4548
|
+
const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4549
|
+
alertStmt.run(alertValues);
|
|
4550
|
+
recordCount++;
|
|
4551
|
+
if (entity.alert?.informedEntity?.length) {
|
|
4552
|
+
for (const informedEntity of entity.alert.informedEntity) {
|
|
4512
4553
|
informedEntity.parent = entity;
|
|
4513
|
-
|
|
4554
|
+
const entityValues = serviceAlertInformedEntities.schema.map(
|
|
4514
4555
|
(column) => prepareRealtimeFieldValue(informedEntity, column, task)
|
|
4515
4556
|
);
|
|
4557
|
+
informedEntityStmt.run(entityValues);
|
|
4558
|
+
recordCount++;
|
|
4516
4559
|
}
|
|
4517
|
-
);
|
|
4518
|
-
for (const values of informedEntities) {
|
|
4519
|
-
informedEntityStmt.run(values);
|
|
4520
4560
|
}
|
|
4561
|
+
} catch (error) {
|
|
4562
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4563
|
+
errorCount++;
|
|
4564
|
+
task.logWarning(`Alert processing error: ${errorMessage}`);
|
|
4521
4565
|
}
|
|
4522
|
-
totalLineCount++;
|
|
4523
|
-
} catch (error) {
|
|
4524
|
-
task.logWarning(`Import error: ${error.message}`);
|
|
4525
4566
|
}
|
|
4526
|
-
}
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
true
|
|
4530
|
-
);
|
|
4531
|
-
})();
|
|
4567
|
+
})();
|
|
4568
|
+
return { recordCount, errorCount };
|
|
4569
|
+
};
|
|
4532
4570
|
}
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
", "
|
|
4538
|
-
)}) VALUES (${tripUpdates.schema.map(() => "?").join(", ")})`
|
|
4571
|
+
function createTripUpdatesProcessor(db, task) {
|
|
4572
|
+
const tripUpdateStmt = createPreparedStatement(
|
|
4573
|
+
db,
|
|
4574
|
+
tripUpdates
|
|
4539
4575
|
);
|
|
4540
|
-
const stopTimeStmt =
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
)}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
|
|
4576
|
+
const stopTimeStmt = createPreparedStatement(
|
|
4577
|
+
db,
|
|
4578
|
+
stopTimeUpdates
|
|
4544
4579
|
);
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
(
|
|
4556
|
-
|
|
4557
|
-
|
|
4580
|
+
return async (batch) => {
|
|
4581
|
+
let recordCount = 0;
|
|
4582
|
+
let errorCount = 0;
|
|
4583
|
+
db.transaction(() => {
|
|
4584
|
+
for (const entity of batch) {
|
|
4585
|
+
try {
|
|
4586
|
+
const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4587
|
+
tripUpdateStmt.run(tripUpdateValues);
|
|
4588
|
+
recordCount++;
|
|
4589
|
+
if (entity.tripUpdate?.stopTimeUpdate?.length) {
|
|
4590
|
+
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
|
|
4591
|
+
stopTimeUpdate.parent = entity;
|
|
4592
|
+
const stopTimeValues = stopTimeUpdates.schema.map(
|
|
4593
|
+
(column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
|
|
4594
|
+
);
|
|
4595
|
+
stopTimeStmt.run(stopTimeValues);
|
|
4596
|
+
recordCount++;
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
} catch (error) {
|
|
4600
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4601
|
+
errorCount++;
|
|
4602
|
+
task.logWarning(`Trip update processing error: ${errorMessage}`);
|
|
4558
4603
|
}
|
|
4559
|
-
totalLineCount++;
|
|
4560
|
-
} catch (error) {
|
|
4561
|
-
task.logWarning(`Import error: ${error.message}`);
|
|
4562
4604
|
}
|
|
4563
|
-
}
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
true
|
|
4567
|
-
);
|
|
4568
|
-
})();
|
|
4605
|
+
})();
|
|
4606
|
+
return { recordCount, errorCount };
|
|
4607
|
+
};
|
|
4569
4608
|
}
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
", "
|
|
4575
|
-
)}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
|
|
4609
|
+
function createVehiclePositionsProcessor(db, task) {
|
|
4610
|
+
const vehiclePositionStmt = createPreparedStatement(
|
|
4611
|
+
db,
|
|
4612
|
+
vehiclePositions
|
|
4576
4613
|
);
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4614
|
+
return async (batch) => {
|
|
4615
|
+
let recordCount = 0;
|
|
4616
|
+
let errorCount = 0;
|
|
4617
|
+
db.transaction(() => {
|
|
4618
|
+
for (const entity of batch) {
|
|
4619
|
+
try {
|
|
4620
|
+
const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4621
|
+
vehiclePositionStmt.run(fieldValues);
|
|
4622
|
+
recordCount++;
|
|
4623
|
+
} catch (error) {
|
|
4624
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4625
|
+
errorCount++;
|
|
4626
|
+
task.logWarning(`Vehicle position processing error: ${errorMessage}`);
|
|
4627
|
+
}
|
|
4585
4628
|
}
|
|
4629
|
+
})();
|
|
4630
|
+
return { recordCount, errorCount };
|
|
4631
|
+
};
|
|
4632
|
+
}
|
|
4633
|
+
function removeExpiredRealtimeData(config) {
|
|
4634
|
+
const db = openDb(config);
|
|
4635
|
+
log(config)(`Removing expired GTFS-Realtime data`);
|
|
4636
|
+
db.transaction(() => {
|
|
4637
|
+
const tables = [
|
|
4638
|
+
"vehicle_positions",
|
|
4639
|
+
"trip_updates",
|
|
4640
|
+
"stop_time_updates",
|
|
4641
|
+
"service_alerts",
|
|
4642
|
+
"service_alert_informed_entities"
|
|
4643
|
+
];
|
|
4644
|
+
for (const table of tables) {
|
|
4645
|
+
db.prepare(
|
|
4646
|
+
`DELETE FROM ${table} WHERE expiration_timestamp <= strftime('%s','now')`
|
|
4647
|
+
).run();
|
|
4586
4648
|
}
|
|
4587
|
-
task.log(
|
|
4588
|
-
`Importing - GTFS-Realtime vehicle positions - ${totalLineCount} entries imported\r`,
|
|
4589
|
-
true
|
|
4590
|
-
);
|
|
4591
4649
|
})();
|
|
4650
|
+
log(config)(`Removed expired GTFS-Realtime data\r`, true);
|
|
4592
4651
|
}
|
|
4593
4652
|
async function updateGtfsRealtimeData(task) {
|
|
4594
|
-
if (task.realtimeAlerts
|
|
4653
|
+
if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
|
|
4595
4654
|
return;
|
|
4596
4655
|
}
|
|
4656
|
+
const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
|
|
4657
|
+
[
|
|
4658
|
+
task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
|
|
4659
|
+
task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
|
|
4660
|
+
task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
|
|
4661
|
+
]
|
|
4662
|
+
);
|
|
4597
4663
|
const db = openDb({ sqlitePath: task.sqlitePath });
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4664
|
+
const recordCounts = {
|
|
4665
|
+
alerts: 0,
|
|
4666
|
+
tripupdates: 0,
|
|
4667
|
+
vehiclepositions: 0
|
|
4668
|
+
};
|
|
4669
|
+
const processingPromises = [];
|
|
4670
|
+
if (alertsData?.entity?.length) {
|
|
4671
|
+
processingPromises.push(
|
|
4672
|
+
processBatch(
|
|
4673
|
+
alertsData.entity,
|
|
4674
|
+
BATCH_SIZE,
|
|
4675
|
+
createServiceAlertsProcessor(db, task)
|
|
4676
|
+
).then((result) => {
|
|
4677
|
+
recordCounts.alerts = result.recordCount;
|
|
4678
|
+
})
|
|
4679
|
+
);
|
|
4611
4680
|
}
|
|
4612
|
-
if (
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
}
|
|
4621
|
-
|
|
4622
|
-
if (task.ignoreErrors) {
|
|
4623
|
-
task.logError(error.message);
|
|
4624
|
-
} else {
|
|
4625
|
-
throw error;
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4681
|
+
if (tripUpdatesData?.entity?.length) {
|
|
4682
|
+
processingPromises.push(
|
|
4683
|
+
processBatch(
|
|
4684
|
+
tripUpdatesData.entity,
|
|
4685
|
+
BATCH_SIZE,
|
|
4686
|
+
createTripUpdatesProcessor(db, task)
|
|
4687
|
+
).then((result) => {
|
|
4688
|
+
recordCounts.tripupdates = result.recordCount;
|
|
4689
|
+
})
|
|
4690
|
+
);
|
|
4628
4691
|
}
|
|
4629
|
-
if (
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
}
|
|
4638
|
-
|
|
4639
|
-
if (task.ignoreErrors) {
|
|
4640
|
-
task.logError(error.message);
|
|
4641
|
-
} else {
|
|
4642
|
-
throw error;
|
|
4643
|
-
}
|
|
4644
|
-
}
|
|
4692
|
+
if (vehiclePositionsData?.entity?.length) {
|
|
4693
|
+
processingPromises.push(
|
|
4694
|
+
processBatch(
|
|
4695
|
+
vehiclePositionsData.entity,
|
|
4696
|
+
BATCH_SIZE,
|
|
4697
|
+
createVehiclePositionsProcessor(db, task)
|
|
4698
|
+
).then((result) => {
|
|
4699
|
+
recordCounts.vehiclepositions = result.recordCount;
|
|
4700
|
+
})
|
|
4701
|
+
);
|
|
4645
4702
|
}
|
|
4646
|
-
|
|
4703
|
+
await Promise.all(processingPromises);
|
|
4704
|
+
task.log(
|
|
4705
|
+
`GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
|
|
4706
|
+
);
|
|
4647
4707
|
}
|
|
4648
4708
|
async function updateGtfsRealtime(initialConfig) {
|
|
4649
4709
|
const config = setDefaultConfig(initialConfig);
|
|
@@ -4653,9 +4713,9 @@ async function updateGtfsRealtime(initialConfig) {
|
|
|
4653
4713
|
const agencyCount = config.agencies.length;
|
|
4654
4714
|
log(config)(
|
|
4655
4715
|
`Starting GTFS-Realtime refresh for ${pluralize(
|
|
4716
|
+
"agency",
|
|
4656
4717
|
"agencies",
|
|
4657
|
-
agencyCount
|
|
4658
|
-
true
|
|
4718
|
+
agencyCount
|
|
4659
4719
|
)} using SQLite database at ${config.sqlitePath}`
|
|
4660
4720
|
);
|
|
4661
4721
|
removeExpiredRealtimeData(config);
|
|
@@ -4677,8 +4737,9 @@ async function updateGtfsRealtime(initialConfig) {
|
|
|
4677
4737
|
};
|
|
4678
4738
|
await updateGtfsRealtimeData(task);
|
|
4679
4739
|
} catch (error) {
|
|
4740
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4680
4741
|
if (config.ignoreErrors) {
|
|
4681
|
-
logError(config)(
|
|
4742
|
+
logError(config)(errorMessage);
|
|
4682
4743
|
} else {
|
|
4683
4744
|
throw error;
|
|
4684
4745
|
}
|
|
@@ -4686,14 +4747,14 @@ async function updateGtfsRealtime(initialConfig) {
|
|
|
4686
4747
|
});
|
|
4687
4748
|
log(config)(
|
|
4688
4749
|
`Completed GTFS-Realtime refresh for ${pluralize(
|
|
4750
|
+
"agency",
|
|
4689
4751
|
"agencies",
|
|
4690
|
-
agencyCount
|
|
4691
|
-
true
|
|
4752
|
+
agencyCount
|
|
4692
4753
|
)}
|
|
4693
4754
|
`
|
|
4694
4755
|
);
|
|
4695
4756
|
} catch (error) {
|
|
4696
|
-
if (error
|
|
4757
|
+
if (error.code === "SQLITE_CANTOPEN") {
|
|
4697
4758
|
logError(config)(
|
|
4698
4759
|
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
4699
4760
|
);
|
|
@@ -4731,8 +4792,8 @@ var extractGtfsFiles = async (task) => {
|
|
|
4731
4792
|
if (!task.path) {
|
|
4732
4793
|
throw new Error("No `path` specified in config");
|
|
4733
4794
|
}
|
|
4734
|
-
const gtfsPath =
|
|
4735
|
-
task.log(`Importing GTFS from ${task.path}\r`);
|
|
4795
|
+
const gtfsPath = untildify(task.path);
|
|
4796
|
+
task.log(`Importing static GTFS from ${task.path}\r`);
|
|
4736
4797
|
if (path2.extname(gtfsPath) === ".zip") {
|
|
4737
4798
|
try {
|
|
4738
4799
|
await unzip(gtfsPath, task.downloadDir);
|
|
@@ -4815,11 +4876,11 @@ var createGtfsTables = (db) => {
|
|
|
4815
4876
|
if (column.type === "time") {
|
|
4816
4877
|
sqlColumnCreateStatements.push(
|
|
4817
4878
|
`${getTimestampColumnName(column.name)} INTEGER GENERATED ALWAYS AS (
|
|
4818
|
-
CASE
|
|
4819
|
-
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
4879
|
+
CASE
|
|
4880
|
+
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
4820
4881
|
ELSE CAST(
|
|
4821
|
-
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
4822
|
-
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
4882
|
+
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
4883
|
+
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
4823
4884
|
substr(${column.name}, -2) AS INTEGER
|
|
4824
4885
|
)
|
|
4825
4886
|
END
|
|
@@ -4876,7 +4937,7 @@ var formatGtfsLine = (line, model, totalLineCount) => {
|
|
|
4876
4937
|
continue;
|
|
4877
4938
|
}
|
|
4878
4939
|
if (type === "date") {
|
|
4879
|
-
value = value.replace(/-/g, "");
|
|
4940
|
+
value = value?.toString().replace(/-/g, "");
|
|
4880
4941
|
if (value.length !== 8) {
|
|
4881
4942
|
throw new Error(
|
|
4882
4943
|
`Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
|
|
@@ -4892,155 +4953,221 @@ var formatGtfsLine = (line, model, totalLineCount) => {
|
|
|
4892
4953
|
}
|
|
4893
4954
|
return formattedLine;
|
|
4894
4955
|
};
|
|
4895
|
-
var
|
|
4896
|
-
var importGtfsFiles = (db, task) =>
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
task.
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
if (model.extension === "gtfs-realtime") {
|
|
4907
|
-
resolve();
|
|
4908
|
-
return;
|
|
4909
|
-
}
|
|
4910
|
-
const filepath = path2.join(task.downloadDir, `${filename}`);
|
|
4911
|
-
if (!existsSync2(filepath)) {
|
|
4912
|
-
if (!model.nonstandard) {
|
|
4913
|
-
task.log(`Importing - ${filename} - No file found\r`);
|
|
4956
|
+
var BATCH_SIZE2 = 1e5;
|
|
4957
|
+
var importGtfsFiles = async (db, task) => {
|
|
4958
|
+
await mapSeries2(
|
|
4959
|
+
Object.values(models_exports),
|
|
4960
|
+
(model) => new Promise((resolve, reject) => {
|
|
4961
|
+
let totalLineCount = 0;
|
|
4962
|
+
const filename = `${model.filenameBase}.${model.filenameExtension}`;
|
|
4963
|
+
if (task.exclude && task.exclude.includes(model.filenameBase)) {
|
|
4964
|
+
task.log(`Skipping - ${filename}\r`);
|
|
4965
|
+
resolve();
|
|
4966
|
+
return;
|
|
4914
4967
|
}
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
|
|
4924
|
-
const insert = db.prepare(prepareStatement);
|
|
4925
|
-
const insertLines = db.transaction((lines) => {
|
|
4926
|
-
for (const [rowNumber, line] of Object.entries(lines)) {
|
|
4927
|
-
try {
|
|
4928
|
-
if (task.prefix === void 0) {
|
|
4929
|
-
insert.run(line);
|
|
4930
|
-
} else {
|
|
4931
|
-
const prefixedLine = Object.fromEntries(
|
|
4932
|
-
Object.entries(
|
|
4933
|
-
line
|
|
4934
|
-
).map(([columnName, value]) => [
|
|
4935
|
-
columnName,
|
|
4936
|
-
applyPrefixToValue(
|
|
4937
|
-
value,
|
|
4938
|
-
prefixedColumns.has(columnName),
|
|
4939
|
-
task.prefix
|
|
4940
|
-
)
|
|
4941
|
-
])
|
|
4942
|
-
);
|
|
4943
|
-
insert.run(prefixedLine);
|
|
4944
|
-
}
|
|
4945
|
-
} catch (error) {
|
|
4946
|
-
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
4947
|
-
const primaryColumns = columns.filter(
|
|
4948
|
-
(column) => column.primary
|
|
4949
|
-
);
|
|
4950
|
-
task.logWarning(
|
|
4951
|
-
`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`
|
|
4952
|
-
);
|
|
4953
|
-
}
|
|
4954
|
-
task.logWarning(
|
|
4955
|
-
`Check ${filename} for invalid data on line ${rowNumber + 1}.`
|
|
4956
|
-
);
|
|
4957
|
-
throw error;
|
|
4968
|
+
if (model.extension === "gtfs-realtime") {
|
|
4969
|
+
resolve();
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
4972
|
+
const filepath = path2.join(task.downloadDir, `${filename}`);
|
|
4973
|
+
if (!existsSync2(filepath)) {
|
|
4974
|
+
if (!model.nonstandard) {
|
|
4975
|
+
task.log(`Importing - ${filename} - No file found\r`);
|
|
4958
4976
|
}
|
|
4977
|
+
resolve();
|
|
4978
|
+
return;
|
|
4959
4979
|
}
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
const
|
|
4963
|
-
columns
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4980
|
+
task.log(`Importing - ${filename}\r`);
|
|
4981
|
+
const columns = model.schema;
|
|
4982
|
+
const prefixedColumns = new Set(
|
|
4983
|
+
columns.filter((column) => column.prefix).map((column) => column.name)
|
|
4984
|
+
);
|
|
4985
|
+
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
|
|
4986
|
+
const insert = db.prepare(prepareStatement);
|
|
4987
|
+
const insertLines = db.transaction((lines) => {
|
|
4988
|
+
for (const [rowNumber, line] of Object.entries(lines)) {
|
|
4989
|
+
try {
|
|
4990
|
+
if (task.prefix === void 0) {
|
|
4991
|
+
insert.run(line);
|
|
4992
|
+
} else {
|
|
4993
|
+
const prefixedLine = Object.fromEntries(
|
|
4994
|
+
Object.entries(
|
|
4995
|
+
line
|
|
4996
|
+
).map(([columnName, value]) => [
|
|
4997
|
+
columnName,
|
|
4998
|
+
applyPrefixToValue(
|
|
4999
|
+
value,
|
|
5000
|
+
prefixedColumns.has(columnName),
|
|
5001
|
+
task.prefix
|
|
5002
|
+
)
|
|
5003
|
+
])
|
|
4982
5004
|
);
|
|
5005
|
+
insert.run(prefixedLine);
|
|
4983
5006
|
}
|
|
5007
|
+
} catch (error) {
|
|
5008
|
+
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
5009
|
+
const primaryColumns = columns.filter(
|
|
5010
|
+
(column) => column.primary
|
|
5011
|
+
);
|
|
5012
|
+
task.logWarning(
|
|
5013
|
+
`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`
|
|
5014
|
+
);
|
|
5015
|
+
}
|
|
5016
|
+
task.logWarning(
|
|
5017
|
+
`Check ${filename} for invalid data on line ${rowNumber + 1}.`
|
|
5018
|
+
);
|
|
5019
|
+
throw error;
|
|
4984
5020
|
}
|
|
4985
|
-
} catch (error) {
|
|
4986
|
-
reject(error);
|
|
4987
5021
|
}
|
|
4988
5022
|
});
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
5023
|
+
if (model.filenameExtension === "txt") {
|
|
5024
|
+
const parser = parse({
|
|
5025
|
+
columns: true,
|
|
5026
|
+
relax_quotes: true,
|
|
5027
|
+
trim: true,
|
|
5028
|
+
skip_empty_lines: true,
|
|
5029
|
+
...task.csvOptions
|
|
5030
|
+
});
|
|
5031
|
+
let lines = [];
|
|
5032
|
+
parser.on("readable", () => {
|
|
5033
|
+
try {
|
|
5034
|
+
let record;
|
|
5035
|
+
while (record = parser.read()) {
|
|
5036
|
+
totalLineCount += 1;
|
|
5037
|
+
lines.push(formatGtfsLine(record, model, totalLineCount));
|
|
5038
|
+
if (lines.length >= BATCH_SIZE2) {
|
|
5039
|
+
insertLines(lines);
|
|
5040
|
+
lines = [];
|
|
5041
|
+
task.log(
|
|
5042
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
5043
|
+
true
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
} catch (error) {
|
|
5048
|
+
if (task.ignoreErrors) {
|
|
5049
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5050
|
+
task.logError(`Error processing ${filename}: ${errorMessage}`);
|
|
5051
|
+
resolve();
|
|
5052
|
+
} else {
|
|
5053
|
+
reject(error);
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
});
|
|
5057
|
+
parser.on("end", () => {
|
|
5058
|
+
try {
|
|
5059
|
+
if (lines.length > 0) {
|
|
5060
|
+
try {
|
|
5061
|
+
insertLines(lines);
|
|
5062
|
+
} catch (error) {
|
|
5063
|
+
if (task.ignoreErrors) {
|
|
5064
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5065
|
+
task.logError(
|
|
5066
|
+
`Error inserting data for ${filename}: ${errorMessage}`
|
|
5067
|
+
);
|
|
5068
|
+
resolve();
|
|
5069
|
+
return;
|
|
5070
|
+
} else {
|
|
5071
|
+
reject(error);
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5076
|
+
task.log(
|
|
5077
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
5078
|
+
true
|
|
5079
|
+
);
|
|
5080
|
+
resolve();
|
|
5081
|
+
} catch (error) {
|
|
5082
|
+
if (task.ignoreErrors) {
|
|
5083
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5084
|
+
task.logError(`Error finalizing ${filename}: ${errorMessage}`);
|
|
5085
|
+
resolve();
|
|
5086
|
+
} else {
|
|
4995
5087
|
reject(error);
|
|
4996
5088
|
}
|
|
4997
5089
|
}
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5090
|
+
});
|
|
5091
|
+
parser.on("error", (error) => {
|
|
5092
|
+
if (task.ignoreErrors) {
|
|
5093
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5094
|
+
task.logError(`Parser error for ${filename}: ${errorMessage}`);
|
|
5095
|
+
resolve();
|
|
5096
|
+
} else {
|
|
5097
|
+
reject(error);
|
|
5098
|
+
}
|
|
5099
|
+
});
|
|
5100
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
5101
|
+
} else if (model.filenameExtension === "geojson") {
|
|
5102
|
+
readFile2(filepath, "utf8").then((data) => {
|
|
5103
|
+
if (isValidJSON(data) === false) {
|
|
5104
|
+
if (task.ignoreErrors) {
|
|
5105
|
+
task.logError(`Invalid JSON in ${filename}`);
|
|
5106
|
+
resolve();
|
|
5107
|
+
return;
|
|
5108
|
+
} else {
|
|
5109
|
+
reject(new Error(`Invalid JSON in ${filename}`));
|
|
5110
|
+
return;
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
totalLineCount += 1;
|
|
5114
|
+
const line = formatGtfsLine(
|
|
5115
|
+
{ geojson: data },
|
|
5116
|
+
model,
|
|
5117
|
+
totalLineCount
|
|
5118
|
+
);
|
|
5119
|
+
try {
|
|
5120
|
+
insertLines([line]);
|
|
5121
|
+
task.log(
|
|
5122
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
5123
|
+
true
|
|
5124
|
+
);
|
|
5125
|
+
resolve();
|
|
5126
|
+
} catch (error) {
|
|
5127
|
+
if (task.ignoreErrors) {
|
|
5128
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5129
|
+
task.logError(
|
|
5130
|
+
`Error inserting data for ${filename}: ${errorMessage}`
|
|
5131
|
+
);
|
|
5132
|
+
resolve();
|
|
5133
|
+
} else {
|
|
5134
|
+
reject(error);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
}).catch((error) => {
|
|
5138
|
+
if (task.ignoreErrors) {
|
|
5139
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5140
|
+
task.logError(`Error reading ${filename}: ${errorMessage}`);
|
|
5141
|
+
resolve();
|
|
5142
|
+
} else {
|
|
5143
|
+
reject(error);
|
|
5144
|
+
}
|
|
5145
|
+
});
|
|
5146
|
+
} else {
|
|
5147
|
+
if (task.ignoreErrors) {
|
|
5148
|
+
task.logError(
|
|
5149
|
+
`Unsupported file type: ${model.filenameExtension} for ${filename}`
|
|
5001
5150
|
);
|
|
5002
5151
|
resolve();
|
|
5003
|
-
}
|
|
5004
|
-
reject(
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
parser.on("error", reject);
|
|
5008
|
-
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
5009
|
-
} else if (model.filenameExtension === "geojson") {
|
|
5010
|
-
readFile2(filepath, "utf8").then((data) => {
|
|
5011
|
-
if (isValidJSON(data) === false) {
|
|
5012
|
-
reject(new Error(`Invalid JSON in ${filename}`));
|
|
5152
|
+
} else {
|
|
5153
|
+
reject(
|
|
5154
|
+
new Error(`Unsupported file type: ${model.filenameExtension}`)
|
|
5155
|
+
);
|
|
5013
5156
|
}
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
);
|
|
5020
|
-
insertLines([line]);
|
|
5021
|
-
task.log(
|
|
5022
|
-
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
5023
|
-
true
|
|
5024
|
-
);
|
|
5025
|
-
resolve();
|
|
5026
|
-
}).catch(reject);
|
|
5027
|
-
} else {
|
|
5028
|
-
reject(
|
|
5029
|
-
new Error(`Unsupported file type: ${model.filenameExtension}`)
|
|
5030
|
-
);
|
|
5031
|
-
}
|
|
5032
|
-
})
|
|
5033
|
-
);
|
|
5157
|
+
}
|
|
5158
|
+
})
|
|
5159
|
+
);
|
|
5160
|
+
task.log(`Static GTFS import complete`);
|
|
5161
|
+
};
|
|
5034
5162
|
async function importGtfs(initialConfig) {
|
|
5035
|
-
const
|
|
5036
|
-
timer.start();
|
|
5163
|
+
const startTime = process.hrtime.bigint();
|
|
5037
5164
|
const config = setDefaultConfig(initialConfig);
|
|
5038
5165
|
validateConfigForImport(config);
|
|
5039
5166
|
try {
|
|
5040
5167
|
const db = openDb(config);
|
|
5041
5168
|
const agencyCount = config.agencies.length;
|
|
5042
5169
|
log(config)(
|
|
5043
|
-
`Starting GTFS import for ${
|
|
5170
|
+
`Starting GTFS import for ${pluralize("file", "files", agencyCount)} using SQLite database at ${config.sqlitePath}`
|
|
5044
5171
|
);
|
|
5045
5172
|
createGtfsTables(db);
|
|
5046
5173
|
await mapSeries2(config.agencies, async (agency2) => {
|
|
@@ -5048,7 +5175,6 @@ async function importGtfs(initialConfig) {
|
|
|
5048
5175
|
const tempPath = temporaryDirectory();
|
|
5049
5176
|
const task = {
|
|
5050
5177
|
exclude: agency2.exclude,
|
|
5051
|
-
url: agency2.url,
|
|
5052
5178
|
headers: agency2.headers,
|
|
5053
5179
|
realtimeAlerts: agency2.realtimeAlerts,
|
|
5054
5180
|
realtimeTripUpdates: agency2.realtimeTripUpdates,
|
|
@@ -5056,7 +5182,6 @@ async function importGtfs(initialConfig) {
|
|
|
5056
5182
|
downloadDir: tempPath,
|
|
5057
5183
|
downloadTimeout: config.downloadTimeout,
|
|
5058
5184
|
gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
|
|
5059
|
-
path: agency2.path,
|
|
5060
5185
|
csvOptions: config.csvOptions || {},
|
|
5061
5186
|
ignoreDuplicates: config.ignoreDuplicates,
|
|
5062
5187
|
ignoreErrors: config.ignoreErrors,
|
|
@@ -5067,8 +5192,13 @@ async function importGtfs(initialConfig) {
|
|
|
5067
5192
|
logWarning: logWarning(config),
|
|
5068
5193
|
logError: logError(config)
|
|
5069
5194
|
};
|
|
5070
|
-
if (
|
|
5195
|
+
if ("url" in agency2) {
|
|
5196
|
+
Object.assign(task, { url: agency2.url });
|
|
5071
5197
|
await downloadGtfsFiles(task);
|
|
5198
|
+
} else {
|
|
5199
|
+
Object.assign(task, {
|
|
5200
|
+
path: agency2.path
|
|
5201
|
+
});
|
|
5072
5202
|
}
|
|
5073
5203
|
await extractGtfsFiles(task);
|
|
5074
5204
|
await importGtfsFiles(db, task);
|
|
@@ -5076,7 +5206,8 @@ async function importGtfs(initialConfig) {
|
|
|
5076
5206
|
await rm2(tempPath, { recursive: true });
|
|
5077
5207
|
} catch (error) {
|
|
5078
5208
|
if (config.ignoreErrors) {
|
|
5079
|
-
|
|
5209
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5210
|
+
logError(config)(errorMessage);
|
|
5080
5211
|
} else {
|
|
5081
5212
|
throw error;
|
|
5082
5213
|
}
|
|
@@ -5084,14 +5215,14 @@ async function importGtfs(initialConfig) {
|
|
|
5084
5215
|
});
|
|
5085
5216
|
log(config)(`Creating DB indexes`);
|
|
5086
5217
|
createGtfsIndexes(db);
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5218
|
+
const endTime = process.hrtime.bigint();
|
|
5219
|
+
const elapsedSeconds = Number(endTime - startTime) / 1e9;
|
|
5089
5220
|
log(config)(
|
|
5090
|
-
`Completed GTFS import
|
|
5221
|
+
`Completed GTFS import in ${elapsedSeconds.toFixed(1)} seconds
|
|
5091
5222
|
`
|
|
5092
5223
|
);
|
|
5093
5224
|
} catch (error) {
|
|
5094
|
-
if (error
|
|
5225
|
+
if (error.code === "SQLITE_CANTOPEN") {
|
|
5095
5226
|
logError(config)(
|
|
5096
5227
|
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
5097
5228
|
);
|
|
@@ -5104,15 +5235,13 @@ async function importGtfs(initialConfig) {
|
|
|
5104
5235
|
import path3 from "path";
|
|
5105
5236
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
5106
5237
|
import { without, compact as compact2 } from "lodash-es";
|
|
5107
|
-
import pluralize3 from "pluralize";
|
|
5108
5238
|
import { stringify } from "csv-stringify";
|
|
5109
5239
|
import sqlString2 from "sqlstring-sqlite";
|
|
5110
5240
|
import mapSeries3 from "promise-map-series";
|
|
5111
|
-
import untildify4 from "untildify";
|
|
5112
5241
|
var getAgencies = (db, config) => {
|
|
5113
5242
|
try {
|
|
5114
5243
|
return db.prepare("SELECT agency_name FROM agency;").all();
|
|
5115
|
-
} catch
|
|
5244
|
+
} catch {
|
|
5116
5245
|
if (config.sqlitePath === ":memory:") {
|
|
5117
5246
|
throw new Error(
|
|
5118
5247
|
'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:".'
|
|
@@ -5138,15 +5267,15 @@ var exportGtfs = async (initialConfig) => {
|
|
|
5138
5267
|
);
|
|
5139
5268
|
}
|
|
5140
5269
|
log(config)(
|
|
5141
|
-
`Starting GTFS export for ${
|
|
5270
|
+
`Starting GTFS export for ${pluralize(
|
|
5142
5271
|
"agency",
|
|
5143
|
-
|
|
5144
|
-
|
|
5272
|
+
"agencies",
|
|
5273
|
+
agencyCount
|
|
5145
5274
|
)} using SQLite database at ${config.sqlitePath}`
|
|
5146
5275
|
);
|
|
5147
5276
|
const folderName = generateFolderName(agencies[0].agency_name);
|
|
5148
5277
|
const defaultExportPath = path3.join(process.cwd(), "gtfs-export", folderName);
|
|
5149
|
-
const exportPath =
|
|
5278
|
+
const exportPath = untildify(config.exportPath || defaultExportPath);
|
|
5150
5279
|
await prepDirectory(exportPath);
|
|
5151
5280
|
const modelsToExport = Object.values(models_exports).filter(
|
|
5152
5281
|
(model) => model.extension !== "gtfs-realtime"
|
|
@@ -5179,11 +5308,17 @@ var exportGtfs = async (initialConfig) => {
|
|
|
5179
5308
|
}
|
|
5180
5309
|
} else if (model.filenameBase === "fare_attributes") {
|
|
5181
5310
|
for (const line of lines) {
|
|
5182
|
-
line.price = formatCurrency(
|
|
5311
|
+
line.price = formatCurrency(
|
|
5312
|
+
line.price,
|
|
5313
|
+
line.currency_type
|
|
5314
|
+
);
|
|
5183
5315
|
}
|
|
5184
5316
|
} else if (model.filenameBase === "fare_products") {
|
|
5185
5317
|
for (const line of lines) {
|
|
5186
|
-
line.amount = formatCurrency(
|
|
5318
|
+
line.amount = formatCurrency(
|
|
5319
|
+
line.amount,
|
|
5320
|
+
line.currency
|
|
5321
|
+
);
|
|
5187
5322
|
}
|
|
5188
5323
|
}
|
|
5189
5324
|
const columns = without(
|
|
@@ -5214,7 +5349,7 @@ var exportGtfs = async (initialConfig) => {
|
|
|
5214
5349
|
}
|
|
5215
5350
|
log(config)(`Completed GTFS export to ${exportPath}`);
|
|
5216
5351
|
log(config)(
|
|
5217
|
-
`Completed GTFS export for ${
|
|
5352
|
+
`Completed GTFS export for ${pluralize("agency", "agencies", agencyCount)}
|
|
5218
5353
|
`
|
|
5219
5354
|
);
|
|
5220
5355
|
};
|
|
@@ -5558,10 +5693,7 @@ function getRoutes(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5558
5693
|
let whereClause = "";
|
|
5559
5694
|
const orderByClause = formatOrderByClause(orderBy2);
|
|
5560
5695
|
const routeQuery = omit3(query, ["stop_id", "service_id"]);
|
|
5561
|
-
const tripQuery = pick(query, [
|
|
5562
|
-
"stop_id",
|
|
5563
|
-
"service_id"
|
|
5564
|
-
]);
|
|
5696
|
+
const tripQuery = pick(query, ["stop_id", "service_id"]);
|
|
5565
5697
|
const whereClauses = Object.entries(routeQuery).map(
|
|
5566
5698
|
([key, value]) => formatWhereClause(key, value)
|
|
5567
5699
|
);
|
|
@@ -5609,7 +5741,12 @@ function getShapes(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5609
5741
|
"service_id",
|
|
5610
5742
|
"direction_id"
|
|
5611
5743
|
]);
|
|
5612
|
-
const tripQuery = pick2(query, [
|
|
5744
|
+
const tripQuery = pick2(query, [
|
|
5745
|
+
"route_id",
|
|
5746
|
+
"trip_id",
|
|
5747
|
+
"service_id",
|
|
5748
|
+
"direction_id"
|
|
5749
|
+
]);
|
|
5613
5750
|
const whereClauses = Object.entries(shapeQuery).map(
|
|
5614
5751
|
([key, value]) => formatWhereClause(key, value)
|
|
5615
5752
|
);
|
|
@@ -5716,7 +5853,7 @@ function getStops(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5716
5853
|
if (options.bounding_box_side_m !== void 0) {
|
|
5717
5854
|
stopQueryOmitKeys.push("stop_lat", "stop_lon");
|
|
5718
5855
|
}
|
|
5719
|
-
|
|
5856
|
+
const stopQuery = omit5(query, stopQueryOmitKeys);
|
|
5720
5857
|
const tripQuery = pick3(query, [
|
|
5721
5858
|
"route_id",
|
|
5722
5859
|
"trip_id",
|
|
@@ -5781,7 +5918,7 @@ function getStoptimes(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5781
5918
|
let whereClause = "";
|
|
5782
5919
|
const orderByClause = formatOrderByClause(orderBy2);
|
|
5783
5920
|
const stoptimeQueryOmitKeys = ["date", "start_time", "end_time"];
|
|
5784
|
-
|
|
5921
|
+
const stoptimeQuery = omit6(query, stoptimeQueryOmitKeys);
|
|
5785
5922
|
const whereClauses = Object.entries(stoptimeQuery).map(
|
|
5786
5923
|
([key, value]) => formatWhereClause(key, value)
|
|
5787
5924
|
);
|
|
@@ -5789,7 +5926,7 @@ function getStoptimes(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5789
5926
|
if (typeof query.date !== "number") {
|
|
5790
5927
|
throw new Error("`date` must be a number in yyyymmdd format");
|
|
5791
5928
|
}
|
|
5792
|
-
const serviceIds = getServiceIdsByDate(query.date);
|
|
5929
|
+
const serviceIds = getServiceIdsByDate(query.date, options);
|
|
5793
5930
|
const tripSubquery = `SELECT DISTINCT trip_id FROM trips WHERE service_id IN (${serviceIds.map((id) => sqlString4.escape(id)).join(",")})`;
|
|
5794
5931
|
whereClauses.push(`trip_id IN (${tripSubquery})`);
|
|
5795
5932
|
}
|
|
@@ -5863,7 +6000,7 @@ function getTrips(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5863
6000
|
let whereClause = "";
|
|
5864
6001
|
const orderByClause = formatOrderByClause(orderBy2);
|
|
5865
6002
|
const tripQueryOmitKeys = ["date"];
|
|
5866
|
-
|
|
6003
|
+
const tripQuery = omit7(query, tripQueryOmitKeys);
|
|
5867
6004
|
const whereClauses = Object.entries(tripQuery).map(
|
|
5868
6005
|
([key, value]) => formatWhereClause(key, value)
|
|
5869
6006
|
);
|
|
@@ -5871,7 +6008,7 @@ function getTrips(query = {}, fields = [], orderBy2 = [], options = {}) {
|
|
|
5871
6008
|
if (typeof query.date !== "number") {
|
|
5872
6009
|
throw new Error("`date` must be a number in yyyymmdd format");
|
|
5873
6010
|
}
|
|
5874
|
-
const serviceIds = getServiceIdsByDate(query.date);
|
|
6011
|
+
const serviceIds = getServiceIdsByDate(query.date, options);
|
|
5875
6012
|
whereClauses.push(
|
|
5876
6013
|
`service_id IN (${serviceIds.map((id) => sqlString5.escape(id)).join(",")})`
|
|
5877
6014
|
);
|
|
@@ -6153,6 +6290,7 @@ export {
|
|
|
6153
6290
|
closeDb,
|
|
6154
6291
|
deleteDb,
|
|
6155
6292
|
exportGtfs,
|
|
6293
|
+
generateFolderName,
|
|
6156
6294
|
getAgencies2 as getAgencies,
|
|
6157
6295
|
getAreas,
|
|
6158
6296
|
getAttributions,
|
|
@@ -6213,6 +6351,9 @@ export {
|
|
|
6213
6351
|
getVehiclePositions,
|
|
6214
6352
|
importGtfs,
|
|
6215
6353
|
openDb,
|
|
6354
|
+
prepDirectory,
|
|
6355
|
+
untildify,
|
|
6356
|
+
unzip,
|
|
6216
6357
|
updateGtfsRealtime
|
|
6217
6358
|
};
|
|
6218
6359
|
//# sourceMappingURL=index.js.map
|