gtfs 4.14.5 → 4.15.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/dist/bin/gtfs-export.js +29 -10
- package/dist/bin/gtfs-export.js.map +1 -1
- package/dist/bin/gtfs-import.js +293 -277
- package/dist/bin/gtfs-import.js.map +1 -1
- package/dist/bin/gtfsrealtime-update.js +171 -145
- package/dist/bin/gtfsrealtime-update.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +356 -337
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/bin/gtfs-import.js
CHANGED
|
@@ -135,18 +135,16 @@ function formatError(error) {
|
|
|
135
135
|
return colors.red(errorMessage);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// src/lib/import.ts
|
|
138
|
+
// src/lib/import-gtfs.ts
|
|
139
139
|
import path2 from "node:path";
|
|
140
140
|
import { createReadStream, existsSync as existsSync2, lstatSync } from "node:fs";
|
|
141
141
|
import { cp, readdir, rename, readFile as readFile2, rm as rm2, writeFile } from "node:fs/promises";
|
|
142
142
|
import { parse } from "csv-parse";
|
|
143
|
-
import
|
|
143
|
+
import pluralize2 from "pluralize";
|
|
144
144
|
import stripBomStream from "strip-bom-stream";
|
|
145
145
|
import { temporaryDirectory } from "tempy";
|
|
146
146
|
import untildify3 from "untildify";
|
|
147
|
-
import
|
|
148
|
-
import GtfsRealtimeBindings from "gtfs-realtime-bindings";
|
|
149
|
-
import sqlString2 from "sqlstring-sqlite";
|
|
147
|
+
import mapSeries2 from "promise-map-series";
|
|
150
148
|
|
|
151
149
|
// src/models/models.ts
|
|
152
150
|
var models_exports = {};
|
|
@@ -3298,6 +3296,12 @@ function isValidJSON(string) {
|
|
|
3298
3296
|
return true;
|
|
3299
3297
|
}
|
|
3300
3298
|
|
|
3299
|
+
// src/lib/import-gtfs-realtime.ts
|
|
3300
|
+
import pluralize from "pluralize";
|
|
3301
|
+
import GtfsRealtimeBindings from "gtfs-realtime-bindings";
|
|
3302
|
+
import sqlString2 from "sqlstring-sqlite";
|
|
3303
|
+
import mapSeries from "promise-map-series";
|
|
3304
|
+
|
|
3301
3305
|
// src/lib/utils.ts
|
|
3302
3306
|
import sqlString from "sqlstring-sqlite";
|
|
3303
3307
|
import Long from "long";
|
|
@@ -3345,60 +3349,10 @@ function padLeadingZeros(time) {
|
|
|
3345
3349
|
return split.join(":");
|
|
3346
3350
|
}
|
|
3347
3351
|
|
|
3348
|
-
// src/lib/import.ts
|
|
3349
|
-
|
|
3350
|
-
if (
|
|
3351
|
-
|
|
3352
|
-
}
|
|
3353
|
-
task.log(`Downloading GTFS from ${task.url}`);
|
|
3354
|
-
task.path = `${task.downloadDir}/gtfs.zip`;
|
|
3355
|
-
const response = await fetch(task.url, {
|
|
3356
|
-
method: "GET",
|
|
3357
|
-
headers: task.headers || {},
|
|
3358
|
-
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
3359
|
-
});
|
|
3360
|
-
if (response.status !== 200) {
|
|
3361
|
-
throw new Error(
|
|
3362
|
-
`Unable to download GTFS from ${task.url}. Got status ${response.status}.`
|
|
3363
|
-
);
|
|
3364
|
-
}
|
|
3365
|
-
const buffer = await response.arrayBuffer();
|
|
3366
|
-
await writeFile(task.path, Buffer.from(buffer));
|
|
3367
|
-
task.log("Download successful");
|
|
3368
|
-
};
|
|
3369
|
-
var downloadGtfsRealtimeData = async (urlAndHeaders, task) => {
|
|
3370
|
-
task.log(`Downloading GTFS-Realtime from ${urlAndHeaders.url}`);
|
|
3371
|
-
const response = await fetch(urlAndHeaders.url, {
|
|
3372
|
-
method: "GET",
|
|
3373
|
-
headers: {
|
|
3374
|
-
...urlAndHeaders.headers ?? {},
|
|
3375
|
-
"Accept-Encoding": "gzip"
|
|
3376
|
-
},
|
|
3377
|
-
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
3378
|
-
});
|
|
3379
|
-
if (response.status !== 200) {
|
|
3380
|
-
task.logWarning(
|
|
3381
|
-
`Unable to download GTFS-Realtime from ${urlAndHeaders.url}. Got status ${response.status}.`
|
|
3382
|
-
);
|
|
3383
|
-
return null;
|
|
3384
|
-
}
|
|
3385
|
-
const buffer = await response.arrayBuffer();
|
|
3386
|
-
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
3387
|
-
new Uint8Array(buffer)
|
|
3388
|
-
);
|
|
3389
|
-
return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
3390
|
-
enums: String,
|
|
3391
|
-
longs: String,
|
|
3392
|
-
bytes: String,
|
|
3393
|
-
defaults: true,
|
|
3394
|
-
arrays: true,
|
|
3395
|
-
objects: true,
|
|
3396
|
-
oneofs: true
|
|
3397
|
-
});
|
|
3398
|
-
};
|
|
3399
|
-
function getDescendantProp(obj, defaultValue, source) {
|
|
3400
|
-
if (source === void 0) return defaultValue;
|
|
3401
|
-
const arr = source.split(".");
|
|
3352
|
+
// src/lib/import-gtfs-realtime.ts
|
|
3353
|
+
function getNestedProperty(obj, defaultValue, path3) {
|
|
3354
|
+
if (path3 === void 0) return defaultValue;
|
|
3355
|
+
const arr = path3.split(".");
|
|
3402
3356
|
while (arr.length) {
|
|
3403
3357
|
const nextKey = arr.shift();
|
|
3404
3358
|
if (nextKey === void 0) {
|
|
@@ -3427,7 +3381,37 @@ function getDescendantProp(obj, defaultValue, source) {
|
|
|
3427
3381
|
if (obj.__isLong__) return convertLongTimeToDate(obj);
|
|
3428
3382
|
return obj;
|
|
3429
3383
|
}
|
|
3430
|
-
|
|
3384
|
+
async function fetchGtfsRealtimeData(urlConfig, task) {
|
|
3385
|
+
task.log(`Downloading GTFS-Realtime from ${urlConfig.url}`);
|
|
3386
|
+
const response = await fetch(urlConfig.url, {
|
|
3387
|
+
method: "GET",
|
|
3388
|
+
headers: {
|
|
3389
|
+
...urlConfig.headers ?? {},
|
|
3390
|
+
"Accept-Encoding": "gzip"
|
|
3391
|
+
},
|
|
3392
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
3393
|
+
});
|
|
3394
|
+
if (response.status !== 200) {
|
|
3395
|
+
task.logWarning(
|
|
3396
|
+
`Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
|
|
3397
|
+
);
|
|
3398
|
+
return null;
|
|
3399
|
+
}
|
|
3400
|
+
const buffer = await response.arrayBuffer();
|
|
3401
|
+
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
3402
|
+
new Uint8Array(buffer)
|
|
3403
|
+
);
|
|
3404
|
+
return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
3405
|
+
enums: String,
|
|
3406
|
+
longs: String,
|
|
3407
|
+
bytes: String,
|
|
3408
|
+
defaults: true,
|
|
3409
|
+
arrays: true,
|
|
3410
|
+
objects: true,
|
|
3411
|
+
oneofs: true
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
function prepareRealtimeFieldValue(entity, column, task) {
|
|
3431
3415
|
if (column.name === "created_timestamp") {
|
|
3432
3416
|
return task.currentTimestamp;
|
|
3433
3417
|
}
|
|
@@ -3435,54 +3419,102 @@ var prepareRealtimeValue = (entity, column, task) => {
|
|
|
3435
3419
|
return task.currentTimestamp + task.gtfsRealtimeExpirationSeconds;
|
|
3436
3420
|
}
|
|
3437
3421
|
return sqlString2.escape(
|
|
3438
|
-
|
|
3422
|
+
getNestedProperty(entity, column.default, column.source)
|
|
3439
3423
|
);
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3424
|
+
}
|
|
3425
|
+
async function processRealtimeAlerts(db, gtfsRealtimeData, task) {
|
|
3426
|
+
task.log(`Download successful`);
|
|
3427
|
+
let totalLineCount = 0;
|
|
3428
|
+
for (const entity of gtfsRealtimeData.entity) {
|
|
3429
|
+
const fieldValues = serviceAlerts.schema.map(
|
|
3430
|
+
(column) => prepareRealtimeFieldValue(entity, column, task)
|
|
3431
|
+
);
|
|
3432
|
+
try {
|
|
3433
|
+
db.prepare(
|
|
3434
|
+
`REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
|
|
3435
|
+
).run();
|
|
3436
|
+
} catch (error) {
|
|
3437
|
+
task.logWarning("Import error: " + error.message);
|
|
3438
|
+
}
|
|
3439
|
+
const alertTargetArray = [];
|
|
3440
|
+
for (const informedEntity of entity.alert.informedEntity) {
|
|
3441
|
+
informedEntity.parent = entity;
|
|
3442
|
+
const subValues = serviceAlertTargets.schema.map(
|
|
3443
|
+
(column) => prepareRealtimeFieldValue(informedEntity, column, task)
|
|
3444
|
+
);
|
|
3445
|
+
alertTargetArray.push(`(${subValues.join(", ")})`);
|
|
3446
|
+
totalLineCount++;
|
|
3447
|
+
}
|
|
3448
|
+
try {
|
|
3449
|
+
db.prepare(
|
|
3450
|
+
`REPLACE INTO ${serviceAlertTargets.filenameBase} (${serviceAlertTargets.schema.map((column) => column.name).join(", ")}) VALUES ${alertTargetArray.join(", ")}`
|
|
3451
|
+
).run();
|
|
3452
|
+
} catch (error) {
|
|
3453
|
+
task.logWarning("Import error: " + error.message);
|
|
3454
|
+
}
|
|
3455
|
+
task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
async function processRealtimeTripUpdates(db, gtfsRealtimeData, task) {
|
|
3459
|
+
task.log(`Download successful`);
|
|
3460
|
+
let totalLineCount = 0;
|
|
3461
|
+
for (const entity of gtfsRealtimeData.entity) {
|
|
3462
|
+
const fieldValues = tripUpdates.schema.map(
|
|
3463
|
+
(column) => prepareRealtimeFieldValue(entity, column, task)
|
|
3464
|
+
);
|
|
3465
|
+
try {
|
|
3466
|
+
db.prepare(
|
|
3467
|
+
`REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
|
|
3468
|
+
).run();
|
|
3469
|
+
} catch (error) {
|
|
3470
|
+
task.logWarning("Import error: " + error.message);
|
|
3471
|
+
}
|
|
3472
|
+
const stopTimeUpdateArray = [];
|
|
3473
|
+
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
|
|
3474
|
+
stopTimeUpdate.parent = entity;
|
|
3475
|
+
const subValues = stopTimeUpdates.schema.map(
|
|
3476
|
+
(column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
|
|
3477
|
+
);
|
|
3478
|
+
stopTimeUpdateArray.push(`(${subValues.join(", ")})`);
|
|
3479
|
+
totalLineCount++;
|
|
3480
|
+
}
|
|
3481
|
+
try {
|
|
3482
|
+
db.prepare(
|
|
3483
|
+
`REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(", ")}) VALUES ${stopTimeUpdateArray.join(", ")}`
|
|
3484
|
+
).run();
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
task.logWarning("Import error: " + error.message);
|
|
3487
|
+
}
|
|
3488
|
+
task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
async function processRealtimeVehiclePositions(db, gtfsRealtimeData, task) {
|
|
3492
|
+
task.log(`Download successful`);
|
|
3493
|
+
let totalLineCount = 0;
|
|
3494
|
+
for (const entity of gtfsRealtimeData.entity) {
|
|
3495
|
+
const fieldValues = vehiclePositions.schema.map(
|
|
3496
|
+
(column) => prepareRealtimeFieldValue(entity, column, task)
|
|
3497
|
+
);
|
|
3498
|
+
try {
|
|
3499
|
+
db.prepare(
|
|
3500
|
+
`REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
|
|
3501
|
+
).run();
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
task.logWarning("Import error: " + error.message);
|
|
3504
|
+
}
|
|
3505
|
+
task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
async function updateGtfsRealtimeData(task) {
|
|
3442
3509
|
if (task.realtimeAlerts === void 0 && task.realtimeTripUpdates === void 0 && task.realtimeVehiclePositions === void 0) {
|
|
3443
3510
|
return;
|
|
3444
3511
|
}
|
|
3445
|
-
const db = openDb({
|
|
3446
|
-
sqlitePath: task.sqlitePath
|
|
3447
|
-
});
|
|
3512
|
+
const db = openDb({ sqlitePath: task.sqlitePath });
|
|
3448
3513
|
if (task.realtimeAlerts?.url) {
|
|
3449
3514
|
try {
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
task
|
|
3453
|
-
);
|
|
3454
|
-
if (gtfsRealtimeData?.entity) {
|
|
3455
|
-
task.log(`Download successful`);
|
|
3456
|
-
let totalLineCount = 0;
|
|
3457
|
-
for (const entity of gtfsRealtimeData.entity) {
|
|
3458
|
-
const fieldValues = serviceAlerts.schema.map(
|
|
3459
|
-
(column) => prepareRealtimeValue(entity, column, task)
|
|
3460
|
-
);
|
|
3461
|
-
try {
|
|
3462
|
-
db.prepare(
|
|
3463
|
-
`REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
|
|
3464
|
-
).run();
|
|
3465
|
-
} catch (error) {
|
|
3466
|
-
task.logWarning("Import error: " + error.message);
|
|
3467
|
-
}
|
|
3468
|
-
const alertTargetArray = [];
|
|
3469
|
-
for (const informedEntity of entity.alert.informedEntity) {
|
|
3470
|
-
informedEntity.parent = entity;
|
|
3471
|
-
const subValues = serviceAlertTargets.schema.map(
|
|
3472
|
-
(column) => prepareRealtimeValue(informedEntity, column, task)
|
|
3473
|
-
);
|
|
3474
|
-
alertTargetArray.push(`(${subValues.join(", ")})`);
|
|
3475
|
-
totalLineCount++;
|
|
3476
|
-
}
|
|
3477
|
-
try {
|
|
3478
|
-
db.prepare(
|
|
3479
|
-
`REPLACE INTO ${serviceAlertTargets.filenameBase} (${serviceAlertTargets.schema.map((column) => column.name).join(", ")}) VALUES ${alertTargetArray.join(", ")}`
|
|
3480
|
-
).run();
|
|
3481
|
-
} catch (error) {
|
|
3482
|
-
task.logWarning("Import error: " + error.message);
|
|
3483
|
-
}
|
|
3484
|
-
task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
|
|
3485
|
-
}
|
|
3515
|
+
const alertsData = await fetchGtfsRealtimeData(task.realtimeAlerts, task);
|
|
3516
|
+
if (alertsData?.entity) {
|
|
3517
|
+
await processRealtimeAlerts(db, alertsData, task);
|
|
3486
3518
|
}
|
|
3487
3519
|
} catch (error) {
|
|
3488
3520
|
if (task.ignoreErrors) {
|
|
@@ -3494,42 +3526,12 @@ var updateRealtimeData = async (task) => {
|
|
|
3494
3526
|
}
|
|
3495
3527
|
if (task.realtimeTripUpdates?.url) {
|
|
3496
3528
|
try {
|
|
3497
|
-
const
|
|
3529
|
+
const tripUpdatesData = await fetchGtfsRealtimeData(
|
|
3498
3530
|
task.realtimeTripUpdates,
|
|
3499
3531
|
task
|
|
3500
3532
|
);
|
|
3501
|
-
if (
|
|
3502
|
-
|
|
3503
|
-
let totalLineCount = 0;
|
|
3504
|
-
for (const entity of gtfsRealtimeData.entity) {
|
|
3505
|
-
const fieldValues = tripUpdates.schema.map(
|
|
3506
|
-
(column) => prepareRealtimeValue(entity, column, task)
|
|
3507
|
-
);
|
|
3508
|
-
try {
|
|
3509
|
-
db.prepare(
|
|
3510
|
-
`REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
|
|
3511
|
-
).run();
|
|
3512
|
-
} catch (error) {
|
|
3513
|
-
task.logWarning("Import error: " + error.message);
|
|
3514
|
-
}
|
|
3515
|
-
const stopTimeUpdateArray = [];
|
|
3516
|
-
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
|
|
3517
|
-
stopTimeUpdate.parent = entity;
|
|
3518
|
-
const subValues = stopTimeUpdates.schema.map(
|
|
3519
|
-
(column) => prepareRealtimeValue(stopTimeUpdate, column, task)
|
|
3520
|
-
);
|
|
3521
|
-
stopTimeUpdateArray.push(`(${subValues.join(", ")})`);
|
|
3522
|
-
totalLineCount++;
|
|
3523
|
-
}
|
|
3524
|
-
try {
|
|
3525
|
-
db.prepare(
|
|
3526
|
-
`REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(", ")}) VALUES ${stopTimeUpdateArray.join(", ")}`
|
|
3527
|
-
).run();
|
|
3528
|
-
} catch (error) {
|
|
3529
|
-
task.logWarning("Import error: " + error.message);
|
|
3530
|
-
}
|
|
3531
|
-
task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
|
|
3532
|
-
}
|
|
3533
|
+
if (tripUpdatesData?.entity) {
|
|
3534
|
+
await processRealtimeTripUpdates(db, tripUpdatesData, task);
|
|
3533
3535
|
}
|
|
3534
3536
|
} catch (error) {
|
|
3535
3537
|
if (task.ignoreErrors) {
|
|
@@ -3541,26 +3543,12 @@ var updateRealtimeData = async (task) => {
|
|
|
3541
3543
|
}
|
|
3542
3544
|
if (task.realtimeVehiclePositions?.url) {
|
|
3543
3545
|
try {
|
|
3544
|
-
const
|
|
3546
|
+
const vehiclePositionsData = await fetchGtfsRealtimeData(
|
|
3545
3547
|
task.realtimeVehiclePositions,
|
|
3546
3548
|
task
|
|
3547
3549
|
);
|
|
3548
|
-
if (
|
|
3549
|
-
|
|
3550
|
-
let totalLineCount = 0;
|
|
3551
|
-
for (const entity of gtfsRealtimeData.entity) {
|
|
3552
|
-
const fieldValues = vehiclePositions.schema.map(
|
|
3553
|
-
(column) => prepareRealtimeValue(entity, column, task)
|
|
3554
|
-
);
|
|
3555
|
-
try {
|
|
3556
|
-
db.prepare(
|
|
3557
|
-
`REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
|
|
3558
|
-
).run();
|
|
3559
|
-
} catch (error) {
|
|
3560
|
-
task.logWarning("Import error: " + error.message);
|
|
3561
|
-
}
|
|
3562
|
-
task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
|
|
3563
|
-
}
|
|
3550
|
+
if (vehiclePositionsData?.entity) {
|
|
3551
|
+
await processRealtimeVehiclePositions(db, vehiclePositionsData, task);
|
|
3564
3552
|
}
|
|
3565
3553
|
} catch (error) {
|
|
3566
3554
|
if (task.ignoreErrors) {
|
|
@@ -3571,12 +3559,59 @@ var updateRealtimeData = async (task) => {
|
|
|
3571
3559
|
}
|
|
3572
3560
|
}
|
|
3573
3561
|
task.log(`GTFS-Realtime data import complete`);
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
// src/lib/import-gtfs.ts
|
|
3565
|
+
var dateCache = {};
|
|
3566
|
+
var calculateAndCacheDate = (value) => {
|
|
3567
|
+
const cached = dateCache[value];
|
|
3568
|
+
if (cached != null) {
|
|
3569
|
+
return cached;
|
|
3570
|
+
}
|
|
3571
|
+
const seconds = calculateSecondsFromMidnight(value);
|
|
3572
|
+
const date = padLeadingZeros(value);
|
|
3573
|
+
const computed = [seconds, date];
|
|
3574
|
+
dateCache[value] = computed;
|
|
3575
|
+
return computed;
|
|
3574
3576
|
};
|
|
3575
3577
|
var getTextFiles = async (folderPath) => {
|
|
3576
3578
|
const files = await readdir(folderPath);
|
|
3577
3579
|
return files.filter((filename) => filename.slice(-3) === "txt");
|
|
3578
3580
|
};
|
|
3579
|
-
var
|
|
3581
|
+
var TIME_COLUMN_NAMES = [
|
|
3582
|
+
"start_time",
|
|
3583
|
+
"end_time",
|
|
3584
|
+
"arrival_time",
|
|
3585
|
+
"departure_time",
|
|
3586
|
+
"prior_notice_last_time",
|
|
3587
|
+
"prior_notice_start_time",
|
|
3588
|
+
"start_pickup_drop_off_window"
|
|
3589
|
+
];
|
|
3590
|
+
var TIME_COLUMN_PAIRS = TIME_COLUMN_NAMES.map((name) => [
|
|
3591
|
+
name,
|
|
3592
|
+
name.endsWith("time") ? `${name}stamp` : `${name}_timestamp`
|
|
3593
|
+
]);
|
|
3594
|
+
var downloadGtfsFiles = async (task) => {
|
|
3595
|
+
if (!task.url) {
|
|
3596
|
+
throw new Error("No `url` specified in config");
|
|
3597
|
+
}
|
|
3598
|
+
task.log(`Downloading GTFS from ${task.url}`);
|
|
3599
|
+
task.path = `${task.downloadDir}/gtfs.zip`;
|
|
3600
|
+
const response = await fetch(task.url, {
|
|
3601
|
+
method: "GET",
|
|
3602
|
+
headers: task.headers || {},
|
|
3603
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
3604
|
+
});
|
|
3605
|
+
if (response.status !== 200) {
|
|
3606
|
+
throw new Error(
|
|
3607
|
+
`Unable to download GTFS from ${task.url}. Got status ${response.status}.`
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
const buffer = await response.arrayBuffer();
|
|
3611
|
+
await writeFile(task.path, Buffer.from(buffer));
|
|
3612
|
+
task.log("Download successful");
|
|
3613
|
+
};
|
|
3614
|
+
var extractGtfsFiles = async (task) => {
|
|
3580
3615
|
if (!task.path) {
|
|
3581
3616
|
throw new Error("No `path` specified in config");
|
|
3582
3617
|
}
|
|
@@ -3628,7 +3663,7 @@ var readFiles = async (task) => {
|
|
|
3628
3663
|
}
|
|
3629
3664
|
}
|
|
3630
3665
|
};
|
|
3631
|
-
var
|
|
3666
|
+
var createGtfsTables = (db) => {
|
|
3632
3667
|
for (const model of Object.values(models_exports)) {
|
|
3633
3668
|
if (!model.schema) {
|
|
3634
3669
|
return;
|
|
@@ -3657,6 +3692,13 @@ var createTables = (db) => {
|
|
|
3657
3692
|
db.prepare(
|
|
3658
3693
|
`CREATE TABLE ${model.filenameBase} (${columns.join(", ")});`
|
|
3659
3694
|
).run();
|
|
3695
|
+
}
|
|
3696
|
+
};
|
|
3697
|
+
var createGtfsIndexes = (db) => {
|
|
3698
|
+
for (const model of Object.values(models_exports)) {
|
|
3699
|
+
if (!model.schema) {
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3660
3702
|
for (const column of model.schema.filter((column2) => column2.index)) {
|
|
3661
3703
|
db.prepare(
|
|
3662
3704
|
`CREATE INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`
|
|
@@ -3664,7 +3706,7 @@ var createTables = (db) => {
|
|
|
3664
3706
|
}
|
|
3665
3707
|
}
|
|
3666
3708
|
};
|
|
3667
|
-
var
|
|
3709
|
+
var formatGtfsLine = (line, model, totalLineCount) => {
|
|
3668
3710
|
const lineNumber = totalLineCount + 1;
|
|
3669
3711
|
const formattedLine = {};
|
|
3670
3712
|
for (const columnSchema of model.schema) {
|
|
@@ -3705,85 +3747,23 @@ var formatLine = (line, model, totalLineCount) => {
|
|
|
3705
3747
|
);
|
|
3706
3748
|
}
|
|
3707
3749
|
}
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
"prior_notice_start_time",
|
|
3715
|
-
"start_pickup_drop_off_window"
|
|
3716
|
-
];
|
|
3717
|
-
for (const timeColumnName of timeColumnNames) {
|
|
3718
|
-
if (formattedLine[timeColumnName]) {
|
|
3719
|
-
const timestampColumnName = timeColumnName.endsWith("time") ? `${timeColumnName}stamp` : `${timeColumnName}_timestamp`;
|
|
3720
|
-
formattedLine[timestampColumnName] = calculateSecondsFromMidnight(
|
|
3721
|
-
formattedLine[timeColumnName]
|
|
3722
|
-
);
|
|
3723
|
-
formattedLine[timeColumnName] = padLeadingZeros(
|
|
3724
|
-
formattedLine[timeColumnName]
|
|
3725
|
-
);
|
|
3750
|
+
for (const [timeColumnName, timestampColumnName] of TIME_COLUMN_PAIRS) {
|
|
3751
|
+
const value = formattedLine[timeColumnName];
|
|
3752
|
+
if (value) {
|
|
3753
|
+
const [seconds, date] = calculateAndCacheDate(value);
|
|
3754
|
+
formattedLine[timestampColumnName] = seconds;
|
|
3755
|
+
formattedLine[timeColumnName] = date;
|
|
3726
3756
|
}
|
|
3727
3757
|
}
|
|
3728
3758
|
return formattedLine;
|
|
3729
3759
|
};
|
|
3730
|
-
var
|
|
3731
|
-
const db = openDb({
|
|
3732
|
-
sqlitePath: task.sqlitePath
|
|
3733
|
-
});
|
|
3734
|
-
if (lines.length === 0) {
|
|
3735
|
-
return;
|
|
3736
|
-
}
|
|
3737
|
-
const linesToImportCount = lines.length;
|
|
3738
|
-
const columns = model.schema.filter((column) => column.name !== "id");
|
|
3739
|
-
const placeholders = [];
|
|
3740
|
-
const values = [];
|
|
3741
|
-
while (lines.length > 0) {
|
|
3742
|
-
const line = lines.pop();
|
|
3743
|
-
if (line === void 0) {
|
|
3744
|
-
continue;
|
|
3745
|
-
}
|
|
3746
|
-
placeholders.push(`(${columns.map(() => "?").join(", ")})`);
|
|
3747
|
-
values.push(
|
|
3748
|
-
...columns.map((column) => {
|
|
3749
|
-
if (task.prefix !== void 0 && column.prefix === true) {
|
|
3750
|
-
return `${task.prefix}${line[column.name]}`;
|
|
3751
|
-
}
|
|
3752
|
-
return line[column.name];
|
|
3753
|
-
})
|
|
3754
|
-
);
|
|
3755
|
-
}
|
|
3756
|
-
try {
|
|
3757
|
-
db.prepare(
|
|
3758
|
-
`INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map((column) => column.name).join(", ")}) VALUES ${placeholders.join(",")}`
|
|
3759
|
-
).run(...values);
|
|
3760
|
-
} catch (error) {
|
|
3761
|
-
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
3762
|
-
const primaryColumns = model.schema.filter((column) => column.primary);
|
|
3763
|
-
task.logWarning(
|
|
3764
|
-
`Duplicate values for primary key (${primaryColumns.map((column) => column.name).join(", ")}) found in ${model.filenameBase}.${model.filenameExtension}. Set the \`ignoreDuplicates\` option to true in config.json to ignore this error`
|
|
3765
|
-
);
|
|
3766
|
-
}
|
|
3767
|
-
task.logWarning(
|
|
3768
|
-
`Check ${model.filenameBase}.${model.filenameExtension} for invalid data between lines ${totalLineCount - linesToImportCount} and ${totalLineCount}.`
|
|
3769
|
-
);
|
|
3770
|
-
throw error;
|
|
3771
|
-
}
|
|
3772
|
-
task.log(
|
|
3773
|
-
`Importing - ${model.filenameBase}.${model.filenameExtension} - ${totalLineCount} lines imported\r`,
|
|
3774
|
-
true
|
|
3775
|
-
);
|
|
3776
|
-
};
|
|
3777
|
-
var importFiles = (task) => mapSeries(
|
|
3760
|
+
var importGtfsFiles = (db, task) => mapSeries2(
|
|
3778
3761
|
Object.values(models_exports),
|
|
3779
3762
|
(model) => new Promise((resolve, reject) => {
|
|
3780
|
-
const lines = [];
|
|
3781
3763
|
let totalLineCount = 0;
|
|
3782
|
-
const
|
|
3764
|
+
const filename = `${model.filenameBase}.${model.filenameExtension}`;
|
|
3783
3765
|
if (task.exclude && task.exclude.includes(model.filenameBase)) {
|
|
3784
|
-
task.log(
|
|
3785
|
-
`Skipping - ${model.filenameBase}.${model.filenameExtension}\r`
|
|
3786
|
-
);
|
|
3766
|
+
task.log(`Skipping - ${filename}\r`);
|
|
3787
3767
|
resolve();
|
|
3788
3768
|
return;
|
|
3789
3769
|
}
|
|
@@ -3791,22 +3771,51 @@ var importFiles = (task) => mapSeries(
|
|
|
3791
3771
|
resolve();
|
|
3792
3772
|
return;
|
|
3793
3773
|
}
|
|
3794
|
-
const filepath = path2.join(
|
|
3795
|
-
task.downloadDir,
|
|
3796
|
-
`${model.filenameBase}.${model.filenameExtension}`
|
|
3797
|
-
);
|
|
3774
|
+
const filepath = path2.join(task.downloadDir, `${filename}`);
|
|
3798
3775
|
if (!existsSync2(filepath)) {
|
|
3799
3776
|
if (!model.nonstandard) {
|
|
3800
|
-
task.log(
|
|
3801
|
-
`Importing - ${model.filenameBase}.${model.filenameExtension} - No file found\r`
|
|
3802
|
-
);
|
|
3777
|
+
task.log(`Importing - ${filename} - No file found\r`);
|
|
3803
3778
|
}
|
|
3804
3779
|
resolve();
|
|
3805
3780
|
return;
|
|
3806
3781
|
}
|
|
3807
|
-
task.log(
|
|
3808
|
-
|
|
3809
|
-
);
|
|
3782
|
+
task.log(`Importing - ${filename}\r`);
|
|
3783
|
+
const columns = model.schema.filter((column) => column.name !== "id");
|
|
3784
|
+
const placeholder = columns.map(({ name }) => `@${name}`).join(", ");
|
|
3785
|
+
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map((column) => column.name).join(", ")}) VALUES (${placeholder})`;
|
|
3786
|
+
const insert = db.prepare(prepareStatement);
|
|
3787
|
+
const insertLines = db.transaction((lines) => {
|
|
3788
|
+
for (const [rowNumber, line] of Object.entries(lines)) {
|
|
3789
|
+
try {
|
|
3790
|
+
if (task.prefix === void 0) {
|
|
3791
|
+
insert.run(line);
|
|
3792
|
+
} else {
|
|
3793
|
+
const prefixedLine = Object.fromEntries(
|
|
3794
|
+
Object.entries(
|
|
3795
|
+
line
|
|
3796
|
+
).map(([columnName, value]) => [
|
|
3797
|
+
columnName,
|
|
3798
|
+
columns.find((col) => col.name === columnName)?.prefix ? `${task.prefix}${value}` : value
|
|
3799
|
+
])
|
|
3800
|
+
);
|
|
3801
|
+
insert.run(prefixedLine);
|
|
3802
|
+
}
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
3805
|
+
const primaryColumns = model.schema.filter(
|
|
3806
|
+
(column) => column.primary
|
|
3807
|
+
);
|
|
3808
|
+
task.logWarning(
|
|
3809
|
+
`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`
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
task.logWarning(
|
|
3813
|
+
`Check ${filename} for invalid data on row ${rowNumber}.`
|
|
3814
|
+
);
|
|
3815
|
+
throw error;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3810
3819
|
if (model.filenameExtension === "txt") {
|
|
3811
3820
|
const parser = parse({
|
|
3812
3821
|
columns: true,
|
|
@@ -3815,23 +3824,21 @@ var importFiles = (task) => mapSeries(
|
|
|
3815
3824
|
skip_empty_lines: true,
|
|
3816
3825
|
...task.csvOptions
|
|
3817
3826
|
});
|
|
3827
|
+
let lines = [];
|
|
3818
3828
|
parser.on("readable", () => {
|
|
3819
3829
|
let record;
|
|
3820
3830
|
while (record = parser.read()) {
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
lines.push(formatLine(record, model, totalLineCount));
|
|
3824
|
-
if (lines.length >= maxInsertVariables / model.schema.length) {
|
|
3825
|
-
importLines(task, lines, model, totalLineCount);
|
|
3826
|
-
}
|
|
3827
|
-
} catch (error) {
|
|
3828
|
-
reject(error);
|
|
3829
|
-
}
|
|
3831
|
+
totalLineCount += 1;
|
|
3832
|
+
lines.push(formatGtfsLine(record, model, totalLineCount));
|
|
3830
3833
|
}
|
|
3831
3834
|
});
|
|
3832
3835
|
parser.on("end", () => {
|
|
3833
3836
|
try {
|
|
3834
|
-
|
|
3837
|
+
insertLines(lines);
|
|
3838
|
+
task.log(
|
|
3839
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
3840
|
+
true
|
|
3841
|
+
);
|
|
3835
3842
|
} catch (error) {
|
|
3836
3843
|
reject(error);
|
|
3837
3844
|
}
|
|
@@ -3842,14 +3849,19 @@ var importFiles = (task) => mapSeries(
|
|
|
3842
3849
|
} else if (model.filenameExtension === "geojson") {
|
|
3843
3850
|
readFile2(filepath, "utf8").then((data) => {
|
|
3844
3851
|
if (isValidJSON(data) === false) {
|
|
3845
|
-
reject(
|
|
3846
|
-
new Error(
|
|
3847
|
-
`Invalid JSON in ${model.filenameBase}.${model.filenameExtension}`
|
|
3848
|
-
)
|
|
3849
|
-
);
|
|
3852
|
+
reject(new Error(`Invalid JSON in ${filename}`));
|
|
3850
3853
|
}
|
|
3851
|
-
|
|
3852
|
-
|
|
3854
|
+
totalLineCount += 1;
|
|
3855
|
+
const line = formatGtfsLine(
|
|
3856
|
+
{ geojson: data },
|
|
3857
|
+
model,
|
|
3858
|
+
totalLineCount
|
|
3859
|
+
);
|
|
3860
|
+
insertLines([line]);
|
|
3861
|
+
task.log(
|
|
3862
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
3863
|
+
true
|
|
3864
|
+
);
|
|
3853
3865
|
resolve();
|
|
3854
3866
|
}).catch(reject);
|
|
3855
3867
|
} else {
|
|
@@ -3869,14 +3881,10 @@ async function importGtfs(initialConfig) {
|
|
|
3869
3881
|
const db = openDb(config);
|
|
3870
3882
|
const agencyCount = config.agencies.length;
|
|
3871
3883
|
log2(
|
|
3872
|
-
`Starting GTFS import for ${
|
|
3873
|
-
"file",
|
|
3874
|
-
agencyCount,
|
|
3875
|
-
true
|
|
3876
|
-
)} using SQLite database at ${config.sqlitePath}`
|
|
3884
|
+
`Starting GTFS import for ${pluralize2("file", agencyCount, true)} using SQLite database at ${config.sqlitePath}`
|
|
3877
3885
|
);
|
|
3878
|
-
|
|
3879
|
-
await
|
|
3886
|
+
createGtfsTables(db);
|
|
3887
|
+
await mapSeries2(config.agencies, async (agency2) => {
|
|
3880
3888
|
try {
|
|
3881
3889
|
const tempPath = temporaryDirectory();
|
|
3882
3890
|
const task = {
|
|
@@ -3901,40 +3909,48 @@ async function importGtfs(initialConfig) {
|
|
|
3901
3909
|
logError: logError2
|
|
3902
3910
|
};
|
|
3903
3911
|
if (task.url) {
|
|
3904
|
-
await
|
|
3912
|
+
await downloadGtfsFiles(task);
|
|
3905
3913
|
}
|
|
3906
|
-
await
|
|
3907
|
-
await
|
|
3908
|
-
await
|
|
3914
|
+
await extractGtfsFiles(task);
|
|
3915
|
+
await importGtfsFiles(db, task);
|
|
3916
|
+
await updateGtfsRealtimeData(task);
|
|
3909
3917
|
await rm2(tempPath, { recursive: true });
|
|
3910
3918
|
} catch (error) {
|
|
3911
|
-
|
|
3912
|
-
logError2(error.message);
|
|
3913
|
-
} else {
|
|
3914
|
-
throw error;
|
|
3915
|
-
}
|
|
3919
|
+
handleImportError(error, config, logError2);
|
|
3916
3920
|
}
|
|
3917
3921
|
});
|
|
3922
|
+
log2(`Creating DB indexes`);
|
|
3923
|
+
createGtfsIndexes(db);
|
|
3918
3924
|
log2(
|
|
3919
|
-
`Completed GTFS import for ${
|
|
3925
|
+
`Completed GTFS import for ${pluralize2("agency", agencyCount, true)}
|
|
3920
3926
|
`
|
|
3921
3927
|
);
|
|
3922
3928
|
} catch (error) {
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3929
|
+
handleDatabaseError(error, config, logError2);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
function handleImportError(error, config, logError2) {
|
|
3933
|
+
if (config.ignoreErrors) {
|
|
3934
|
+
logError2(error.message);
|
|
3935
|
+
} else {
|
|
3928
3936
|
throw error;
|
|
3929
3937
|
}
|
|
3930
3938
|
}
|
|
3939
|
+
function handleDatabaseError(error, config, logError2) {
|
|
3940
|
+
if (error?.code === "SQLITE_CANTOPEN") {
|
|
3941
|
+
logError2(
|
|
3942
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
throw error;
|
|
3946
|
+
}
|
|
3931
3947
|
|
|
3932
3948
|
// src/lib/export.ts
|
|
3933
3949
|
import { without, compact as compact2 } from "lodash-es";
|
|
3934
|
-
import
|
|
3950
|
+
import pluralize3 from "pluralize";
|
|
3935
3951
|
import { stringify } from "csv-stringify";
|
|
3936
3952
|
import sqlString3 from "sqlstring-sqlite";
|
|
3937
|
-
import
|
|
3953
|
+
import mapSeries3 from "promise-map-series";
|
|
3938
3954
|
import untildify4 from "untildify";
|
|
3939
3955
|
|
|
3940
3956
|
// src/lib/advancedQuery.ts
|