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/index.js CHANGED
@@ -4,18 +4,16 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
- // src/lib/import.ts
7
+ // src/lib/import-gtfs.ts
8
8
  import path from "node:path";
9
9
  import { createReadStream, existsSync, lstatSync } from "node:fs";
10
10
  import { cp, readdir, rename, readFile as readFile2, rm as rm2, writeFile } from "node:fs/promises";
11
11
  import { parse } from "csv-parse";
12
- import pluralize from "pluralize";
12
+ import pluralize2 from "pluralize";
13
13
  import stripBomStream from "strip-bom-stream";
14
14
  import { temporaryDirectory } from "tempy";
15
15
  import untildify3 from "untildify";
16
- import mapSeries from "promise-map-series";
17
- import GtfsRealtimeBindings from "gtfs-realtime-bindings";
18
- import sqlString2 from "sqlstring-sqlite";
16
+ import mapSeries2 from "promise-map-series";
19
17
 
20
18
  // src/models/models.ts
21
19
  var models_exports = {};
@@ -3309,6 +3307,12 @@ function stopsToGeoJSONFeatureCollection(stops2) {
3309
3307
  return featureCollection(features);
3310
3308
  }
3311
3309
 
3310
+ // src/lib/import-gtfs-realtime.ts
3311
+ import pluralize from "pluralize";
3312
+ import GtfsRealtimeBindings from "gtfs-realtime-bindings";
3313
+ import sqlString2 from "sqlstring-sqlite";
3314
+ import mapSeries from "promise-map-series";
3315
+
3312
3316
  // src/lib/log-utils.ts
3313
3317
  import { clearLine, cursorTo } from "node:readline";
3314
3318
  import { noop } from "lodash-es";
@@ -3480,60 +3484,10 @@ function formatOrderByClause(orderBy2) {
3480
3484
  return orderByClause;
3481
3485
  }
3482
3486
 
3483
- // src/lib/import.ts
3484
- var downloadFiles = async (task) => {
3485
- if (!task.url) {
3486
- throw new Error("No `url` specified in config");
3487
- }
3488
- task.log(`Downloading GTFS from ${task.url}`);
3489
- task.path = `${task.downloadDir}/gtfs.zip`;
3490
- const response = await fetch(task.url, {
3491
- method: "GET",
3492
- headers: task.headers || {},
3493
- signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
3494
- });
3495
- if (response.status !== 200) {
3496
- throw new Error(
3497
- `Unable to download GTFS from ${task.url}. Got status ${response.status}.`
3498
- );
3499
- }
3500
- const buffer = await response.arrayBuffer();
3501
- await writeFile(task.path, Buffer.from(buffer));
3502
- task.log("Download successful");
3503
- };
3504
- var downloadGtfsRealtimeData = async (urlAndHeaders, task) => {
3505
- task.log(`Downloading GTFS-Realtime from ${urlAndHeaders.url}`);
3506
- const response = await fetch(urlAndHeaders.url, {
3507
- method: "GET",
3508
- headers: {
3509
- ...urlAndHeaders.headers ?? {},
3510
- "Accept-Encoding": "gzip"
3511
- },
3512
- signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
3513
- });
3514
- if (response.status !== 200) {
3515
- task.logWarning(
3516
- `Unable to download GTFS-Realtime from ${urlAndHeaders.url}. Got status ${response.status}.`
3517
- );
3518
- return null;
3519
- }
3520
- const buffer = await response.arrayBuffer();
3521
- const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
3522
- new Uint8Array(buffer)
3523
- );
3524
- return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
3525
- enums: String,
3526
- longs: String,
3527
- bytes: String,
3528
- defaults: true,
3529
- arrays: true,
3530
- objects: true,
3531
- oneofs: true
3532
- });
3533
- };
3534
- function getDescendantProp(obj, defaultValue, source) {
3535
- if (source === void 0) return defaultValue;
3536
- const arr = source.split(".");
3487
+ // src/lib/import-gtfs-realtime.ts
3488
+ function getNestedProperty(obj, defaultValue, path3) {
3489
+ if (path3 === void 0) return defaultValue;
3490
+ const arr = path3.split(".");
3537
3491
  while (arr.length) {
3538
3492
  const nextKey = arr.shift();
3539
3493
  if (nextKey === void 0) {
@@ -3562,7 +3516,37 @@ function getDescendantProp(obj, defaultValue, source) {
3562
3516
  if (obj.__isLong__) return convertLongTimeToDate(obj);
3563
3517
  return obj;
3564
3518
  }
3565
- var deleteExpiredRealtimeData = (config) => {
3519
+ async function fetchGtfsRealtimeData(urlConfig, task) {
3520
+ task.log(`Downloading GTFS-Realtime from ${urlConfig.url}`);
3521
+ const response = await fetch(urlConfig.url, {
3522
+ method: "GET",
3523
+ headers: {
3524
+ ...urlConfig.headers ?? {},
3525
+ "Accept-Encoding": "gzip"
3526
+ },
3527
+ signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
3528
+ });
3529
+ if (response.status !== 200) {
3530
+ task.logWarning(
3531
+ `Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
3532
+ );
3533
+ return null;
3534
+ }
3535
+ const buffer = await response.arrayBuffer();
3536
+ const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
3537
+ new Uint8Array(buffer)
3538
+ );
3539
+ return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
3540
+ enums: String,
3541
+ longs: String,
3542
+ bytes: String,
3543
+ defaults: true,
3544
+ arrays: true,
3545
+ objects: true,
3546
+ oneofs: true
3547
+ });
3548
+ }
3549
+ function removeExpiredRealtimeData(config) {
3566
3550
  const log2 = log(config);
3567
3551
  const db = openDb(config);
3568
3552
  log2(`Removing expired GTFS-Realtime data`);
@@ -3582,8 +3566,8 @@ var deleteExpiredRealtimeData = (config) => {
3582
3566
  `DELETE FROM service_alert_targets WHERE expiration_timestamp <= strftime('%s','now')`
3583
3567
  ).run();
3584
3568
  log2(`Removed expired GTFS-Realtime data\r`, true);
3585
- };
3586
- var prepareRealtimeValue = (entity, column, task) => {
3569
+ }
3570
+ function prepareRealtimeFieldValue(entity, column, task) {
3587
3571
  if (column.name === "created_timestamp") {
3588
3572
  return task.currentTimestamp;
3589
3573
  }
@@ -3591,54 +3575,102 @@ var prepareRealtimeValue = (entity, column, task) => {
3591
3575
  return task.currentTimestamp + task.gtfsRealtimeExpirationSeconds;
3592
3576
  }
3593
3577
  return sqlString2.escape(
3594
- getDescendantProp(entity, column.default, column.source)
3578
+ getNestedProperty(entity, column.default, column.source)
3595
3579
  );
3596
- };
3597
- var updateRealtimeData = async (task) => {
3580
+ }
3581
+ async function processRealtimeAlerts(db, gtfsRealtimeData, task) {
3582
+ task.log(`Download successful`);
3583
+ let totalLineCount = 0;
3584
+ for (const entity of gtfsRealtimeData.entity) {
3585
+ const fieldValues = serviceAlerts.schema.map(
3586
+ (column) => prepareRealtimeFieldValue(entity, column, task)
3587
+ );
3588
+ try {
3589
+ db.prepare(
3590
+ `REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
3591
+ ).run();
3592
+ } catch (error) {
3593
+ task.logWarning("Import error: " + error.message);
3594
+ }
3595
+ const alertTargetArray = [];
3596
+ for (const informedEntity of entity.alert.informedEntity) {
3597
+ informedEntity.parent = entity;
3598
+ const subValues = serviceAlertTargets.schema.map(
3599
+ (column) => prepareRealtimeFieldValue(informedEntity, column, task)
3600
+ );
3601
+ alertTargetArray.push(`(${subValues.join(", ")})`);
3602
+ totalLineCount++;
3603
+ }
3604
+ try {
3605
+ db.prepare(
3606
+ `REPLACE INTO ${serviceAlertTargets.filenameBase} (${serviceAlertTargets.schema.map((column) => column.name).join(", ")}) VALUES ${alertTargetArray.join(", ")}`
3607
+ ).run();
3608
+ } catch (error) {
3609
+ task.logWarning("Import error: " + error.message);
3610
+ }
3611
+ task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
3612
+ }
3613
+ }
3614
+ async function processRealtimeTripUpdates(db, gtfsRealtimeData, task) {
3615
+ task.log(`Download successful`);
3616
+ let totalLineCount = 0;
3617
+ for (const entity of gtfsRealtimeData.entity) {
3618
+ const fieldValues = tripUpdates.schema.map(
3619
+ (column) => prepareRealtimeFieldValue(entity, column, task)
3620
+ );
3621
+ try {
3622
+ db.prepare(
3623
+ `REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
3624
+ ).run();
3625
+ } catch (error) {
3626
+ task.logWarning("Import error: " + error.message);
3627
+ }
3628
+ const stopTimeUpdateArray = [];
3629
+ for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
3630
+ stopTimeUpdate.parent = entity;
3631
+ const subValues = stopTimeUpdates.schema.map(
3632
+ (column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
3633
+ );
3634
+ stopTimeUpdateArray.push(`(${subValues.join(", ")})`);
3635
+ totalLineCount++;
3636
+ }
3637
+ try {
3638
+ db.prepare(
3639
+ `REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(", ")}) VALUES ${stopTimeUpdateArray.join(", ")}`
3640
+ ).run();
3641
+ } catch (error) {
3642
+ task.logWarning("Import error: " + error.message);
3643
+ }
3644
+ task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
3645
+ }
3646
+ }
3647
+ async function processRealtimeVehiclePositions(db, gtfsRealtimeData, task) {
3648
+ task.log(`Download successful`);
3649
+ let totalLineCount = 0;
3650
+ for (const entity of gtfsRealtimeData.entity) {
3651
+ const fieldValues = vehiclePositions.schema.map(
3652
+ (column) => prepareRealtimeFieldValue(entity, column, task)
3653
+ );
3654
+ try {
3655
+ db.prepare(
3656
+ `REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
3657
+ ).run();
3658
+ } catch (error) {
3659
+ task.logWarning("Import error: " + error.message);
3660
+ }
3661
+ task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
3662
+ }
3663
+ }
3664
+ async function updateGtfsRealtimeData(task) {
3598
3665
  if (task.realtimeAlerts === void 0 && task.realtimeTripUpdates === void 0 && task.realtimeVehiclePositions === void 0) {
3599
3666
  return;
3600
3667
  }
3601
- const db = openDb({
3602
- sqlitePath: task.sqlitePath
3603
- });
3668
+ const db = openDb({ sqlitePath: task.sqlitePath });
3604
3669
  if (task.realtimeAlerts?.url) {
3605
3670
  try {
3606
- const gtfsRealtimeData = await downloadGtfsRealtimeData(
3607
- task.realtimeAlerts,
3608
- task
3609
- );
3610
- if (gtfsRealtimeData?.entity) {
3611
- task.log(`Download successful`);
3612
- let totalLineCount = 0;
3613
- for (const entity of gtfsRealtimeData.entity) {
3614
- const fieldValues = serviceAlerts.schema.map(
3615
- (column) => prepareRealtimeValue(entity, column, task)
3616
- );
3617
- try {
3618
- db.prepare(
3619
- `REPLACE INTO ${serviceAlerts.filenameBase} (${serviceAlerts.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
3620
- ).run();
3621
- } catch (error) {
3622
- task.logWarning("Import error: " + error.message);
3623
- }
3624
- const alertTargetArray = [];
3625
- for (const informedEntity of entity.alert.informedEntity) {
3626
- informedEntity.parent = entity;
3627
- const subValues = serviceAlertTargets.schema.map(
3628
- (column) => prepareRealtimeValue(informedEntity, column, task)
3629
- );
3630
- alertTargetArray.push(`(${subValues.join(", ")})`);
3631
- totalLineCount++;
3632
- }
3633
- try {
3634
- db.prepare(
3635
- `REPLACE INTO ${serviceAlertTargets.filenameBase} (${serviceAlertTargets.schema.map((column) => column.name).join(", ")}) VALUES ${alertTargetArray.join(", ")}`
3636
- ).run();
3637
- } catch (error) {
3638
- task.logWarning("Import error: " + error.message);
3639
- }
3640
- task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
3641
- }
3671
+ const alertsData = await fetchGtfsRealtimeData(task.realtimeAlerts, task);
3672
+ if (alertsData?.entity) {
3673
+ await processRealtimeAlerts(db, alertsData, task);
3642
3674
  }
3643
3675
  } catch (error) {
3644
3676
  if (task.ignoreErrors) {
@@ -3650,42 +3682,12 @@ var updateRealtimeData = async (task) => {
3650
3682
  }
3651
3683
  if (task.realtimeTripUpdates?.url) {
3652
3684
  try {
3653
- const gtfsRealtimeData = await downloadGtfsRealtimeData(
3685
+ const tripUpdatesData = await fetchGtfsRealtimeData(
3654
3686
  task.realtimeTripUpdates,
3655
3687
  task
3656
3688
  );
3657
- if (gtfsRealtimeData?.entity) {
3658
- task.log(`Download successful`);
3659
- let totalLineCount = 0;
3660
- for (const entity of gtfsRealtimeData.entity) {
3661
- const fieldValues = tripUpdates.schema.map(
3662
- (column) => prepareRealtimeValue(entity, column, task)
3663
- );
3664
- try {
3665
- db.prepare(
3666
- `REPLACE INTO ${tripUpdates.filenameBase} (${tripUpdates.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
3667
- ).run();
3668
- } catch (error) {
3669
- task.logWarning("Import error: " + error.message);
3670
- }
3671
- const stopTimeUpdateArray = [];
3672
- for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
3673
- stopTimeUpdate.parent = entity;
3674
- const subValues = stopTimeUpdates.schema.map(
3675
- (column) => prepareRealtimeValue(stopTimeUpdate, column, task)
3676
- );
3677
- stopTimeUpdateArray.push(`(${subValues.join(", ")})`);
3678
- totalLineCount++;
3679
- }
3680
- try {
3681
- db.prepare(
3682
- `REPLACE INTO ${stopTimeUpdates.filenameBase} (${stopTimeUpdates.schema.map((column) => column.name).join(", ")}) VALUES ${stopTimeUpdateArray.join(", ")}`
3683
- ).run();
3684
- } catch (error) {
3685
- task.logWarning("Import error: " + error.message);
3686
- }
3687
- task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
3688
- }
3689
+ if (tripUpdatesData?.entity) {
3690
+ await processRealtimeTripUpdates(db, tripUpdatesData, task);
3689
3691
  }
3690
3692
  } catch (error) {
3691
3693
  if (task.ignoreErrors) {
@@ -3697,26 +3699,12 @@ var updateRealtimeData = async (task) => {
3697
3699
  }
3698
3700
  if (task.realtimeVehiclePositions?.url) {
3699
3701
  try {
3700
- const gtfsRealtimeData = await downloadGtfsRealtimeData(
3702
+ const vehiclePositionsData = await fetchGtfsRealtimeData(
3701
3703
  task.realtimeVehiclePositions,
3702
3704
  task
3703
3705
  );
3704
- if (gtfsRealtimeData?.entity) {
3705
- task.log(`Download successful`);
3706
- let totalLineCount = 0;
3707
- for (const entity of gtfsRealtimeData.entity) {
3708
- const fieldValues = vehiclePositions.schema.map(
3709
- (column) => prepareRealtimeValue(entity, column, task)
3710
- );
3711
- try {
3712
- db.prepare(
3713
- `REPLACE INTO ${vehiclePositions.filenameBase} (${vehiclePositions.schema.map((column) => column.name).join(", ")}) VALUES (${fieldValues.join(", ")})`
3714
- ).run();
3715
- } catch (error) {
3716
- task.logWarning("Import error: " + error.message);
3717
- }
3718
- task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
3719
- }
3706
+ if (vehiclePositionsData?.entity) {
3707
+ await processRealtimeVehiclePositions(db, vehiclePositionsData, task);
3720
3708
  }
3721
3709
  } catch (error) {
3722
3710
  if (task.ignoreErrors) {
@@ -3727,12 +3715,120 @@ var updateRealtimeData = async (task) => {
3727
3715
  }
3728
3716
  }
3729
3717
  task.log(`GTFS-Realtime data import complete`);
3718
+ }
3719
+ async function updateGtfsRealtime(initialConfig) {
3720
+ const config = setDefaultConfig(initialConfig);
3721
+ validateConfigForImport(config);
3722
+ const log2 = log(config);
3723
+ const logError2 = logError(config);
3724
+ const logWarning2 = logWarning(config);
3725
+ try {
3726
+ openDb(config);
3727
+ const agencyCount = config.agencies.length;
3728
+ log2(
3729
+ `Starting GTFS-Realtime refresh for ${pluralize(
3730
+ "agencies",
3731
+ agencyCount,
3732
+ true
3733
+ )} using SQLite database at ${config.sqlitePath}`
3734
+ );
3735
+ removeExpiredRealtimeData(config);
3736
+ await mapSeries(config.agencies, async (agency2) => {
3737
+ try {
3738
+ const task = {
3739
+ realtimeAlerts: agency2.realtimeAlerts,
3740
+ realtimeTripUpdates: agency2.realtimeTripUpdates,
3741
+ realtimeVehiclePositions: agency2.realtimeVehiclePositions,
3742
+ downloadTimeout: config.downloadTimeout,
3743
+ gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
3744
+ ignoreErrors: config.ignoreErrors,
3745
+ sqlitePath: config.sqlitePath,
3746
+ currentTimestamp: Math.floor(Date.now() / 1e3),
3747
+ log: log2,
3748
+ logWarning: logWarning2,
3749
+ logError: logError2
3750
+ };
3751
+ await updateGtfsRealtimeData(task);
3752
+ } catch (error) {
3753
+ if (config.ignoreErrors) {
3754
+ logError2(error.message);
3755
+ } else {
3756
+ throw error;
3757
+ }
3758
+ }
3759
+ });
3760
+ log2(
3761
+ `Completed GTFS-Realtime refresh for ${pluralize(
3762
+ "agencies",
3763
+ agencyCount,
3764
+ true
3765
+ )}
3766
+ `
3767
+ );
3768
+ } catch (error) {
3769
+ handleDatabaseError(error, config, logError2);
3770
+ }
3771
+ }
3772
+ function handleDatabaseError(error, config, logError2) {
3773
+ if (error?.code === "SQLITE_CANTOPEN") {
3774
+ logError2(
3775
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
3776
+ );
3777
+ }
3778
+ throw error;
3779
+ }
3780
+
3781
+ // src/lib/import-gtfs.ts
3782
+ var dateCache = {};
3783
+ var calculateAndCacheDate = (value) => {
3784
+ const cached = dateCache[value];
3785
+ if (cached != null) {
3786
+ return cached;
3787
+ }
3788
+ const seconds = calculateSecondsFromMidnight(value);
3789
+ const date = padLeadingZeros(value);
3790
+ const computed = [seconds, date];
3791
+ dateCache[value] = computed;
3792
+ return computed;
3730
3793
  };
3731
3794
  var getTextFiles = async (folderPath) => {
3732
3795
  const files = await readdir(folderPath);
3733
3796
  return files.filter((filename) => filename.slice(-3) === "txt");
3734
3797
  };
3735
- var readFiles = async (task) => {
3798
+ var TIME_COLUMN_NAMES = [
3799
+ "start_time",
3800
+ "end_time",
3801
+ "arrival_time",
3802
+ "departure_time",
3803
+ "prior_notice_last_time",
3804
+ "prior_notice_start_time",
3805
+ "start_pickup_drop_off_window"
3806
+ ];
3807
+ var TIME_COLUMN_PAIRS = TIME_COLUMN_NAMES.map((name) => [
3808
+ name,
3809
+ name.endsWith("time") ? `${name}stamp` : `${name}_timestamp`
3810
+ ]);
3811
+ var downloadGtfsFiles = async (task) => {
3812
+ if (!task.url) {
3813
+ throw new Error("No `url` specified in config");
3814
+ }
3815
+ task.log(`Downloading GTFS from ${task.url}`);
3816
+ task.path = `${task.downloadDir}/gtfs.zip`;
3817
+ const response = await fetch(task.url, {
3818
+ method: "GET",
3819
+ headers: task.headers || {},
3820
+ signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
3821
+ });
3822
+ if (response.status !== 200) {
3823
+ throw new Error(
3824
+ `Unable to download GTFS from ${task.url}. Got status ${response.status}.`
3825
+ );
3826
+ }
3827
+ const buffer = await response.arrayBuffer();
3828
+ await writeFile(task.path, Buffer.from(buffer));
3829
+ task.log("Download successful");
3830
+ };
3831
+ var extractGtfsFiles = async (task) => {
3736
3832
  if (!task.path) {
3737
3833
  throw new Error("No `path` specified in config");
3738
3834
  }
@@ -3784,7 +3880,7 @@ var readFiles = async (task) => {
3784
3880
  }
3785
3881
  }
3786
3882
  };
3787
- var createTables = (db) => {
3883
+ var createGtfsTables = (db) => {
3788
3884
  for (const model of Object.values(models_exports)) {
3789
3885
  if (!model.schema) {
3790
3886
  return;
@@ -3813,6 +3909,13 @@ var createTables = (db) => {
3813
3909
  db.prepare(
3814
3910
  `CREATE TABLE ${model.filenameBase} (${columns.join(", ")});`
3815
3911
  ).run();
3912
+ }
3913
+ };
3914
+ var createGtfsIndexes = (db) => {
3915
+ for (const model of Object.values(models_exports)) {
3916
+ if (!model.schema) {
3917
+ return;
3918
+ }
3816
3919
  for (const column of model.schema.filter((column2) => column2.index)) {
3817
3920
  db.prepare(
3818
3921
  `CREATE INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`
@@ -3820,7 +3923,7 @@ var createTables = (db) => {
3820
3923
  }
3821
3924
  }
3822
3925
  };
3823
- var formatLine = (line, model, totalLineCount) => {
3926
+ var formatGtfsLine = (line, model, totalLineCount) => {
3824
3927
  const lineNumber = totalLineCount + 1;
3825
3928
  const formattedLine = {};
3826
3929
  for (const columnSchema of model.schema) {
@@ -3861,85 +3964,23 @@ var formatLine = (line, model, totalLineCount) => {
3861
3964
  );
3862
3965
  }
3863
3966
  }
3864
- const timeColumnNames = [
3865
- "start_time",
3866
- "end_time",
3867
- "arrival_time",
3868
- "departure_time",
3869
- "prior_notice_last_time",
3870
- "prior_notice_start_time",
3871
- "start_pickup_drop_off_window"
3872
- ];
3873
- for (const timeColumnName of timeColumnNames) {
3874
- if (formattedLine[timeColumnName]) {
3875
- const timestampColumnName = timeColumnName.endsWith("time") ? `${timeColumnName}stamp` : `${timeColumnName}_timestamp`;
3876
- formattedLine[timestampColumnName] = calculateSecondsFromMidnight(
3877
- formattedLine[timeColumnName]
3878
- );
3879
- formattedLine[timeColumnName] = padLeadingZeros(
3880
- formattedLine[timeColumnName]
3881
- );
3967
+ for (const [timeColumnName, timestampColumnName] of TIME_COLUMN_PAIRS) {
3968
+ const value = formattedLine[timeColumnName];
3969
+ if (value) {
3970
+ const [seconds, date] = calculateAndCacheDate(value);
3971
+ formattedLine[timestampColumnName] = seconds;
3972
+ formattedLine[timeColumnName] = date;
3882
3973
  }
3883
3974
  }
3884
3975
  return formattedLine;
3885
3976
  };
3886
- var importLines = (task, lines, model, totalLineCount) => {
3887
- const db = openDb({
3888
- sqlitePath: task.sqlitePath
3889
- });
3890
- if (lines.length === 0) {
3891
- return;
3892
- }
3893
- const linesToImportCount = lines.length;
3894
- const columns = model.schema.filter((column) => column.name !== "id");
3895
- const placeholders = [];
3896
- const values = [];
3897
- while (lines.length > 0) {
3898
- const line = lines.pop();
3899
- if (line === void 0) {
3900
- continue;
3901
- }
3902
- placeholders.push(`(${columns.map(() => "?").join(", ")})`);
3903
- values.push(
3904
- ...columns.map((column) => {
3905
- if (task.prefix !== void 0 && column.prefix === true) {
3906
- return `${task.prefix}${line[column.name]}`;
3907
- }
3908
- return line[column.name];
3909
- })
3910
- );
3911
- }
3912
- try {
3913
- db.prepare(
3914
- `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map((column) => column.name).join(", ")}) VALUES ${placeholders.join(",")}`
3915
- ).run(...values);
3916
- } catch (error) {
3917
- if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
3918
- const primaryColumns = model.schema.filter((column) => column.primary);
3919
- task.logWarning(
3920
- `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`
3921
- );
3922
- }
3923
- task.logWarning(
3924
- `Check ${model.filenameBase}.${model.filenameExtension} for invalid data between lines ${totalLineCount - linesToImportCount} and ${totalLineCount}.`
3925
- );
3926
- throw error;
3927
- }
3928
- task.log(
3929
- `Importing - ${model.filenameBase}.${model.filenameExtension} - ${totalLineCount} lines imported\r`,
3930
- true
3931
- );
3932
- };
3933
- var importFiles = (task) => mapSeries(
3977
+ var importGtfsFiles = (db, task) => mapSeries2(
3934
3978
  Object.values(models_exports),
3935
3979
  (model) => new Promise((resolve, reject) => {
3936
- const lines = [];
3937
3980
  let totalLineCount = 0;
3938
- const maxInsertVariables = 32e3;
3981
+ const filename = `${model.filenameBase}.${model.filenameExtension}`;
3939
3982
  if (task.exclude && task.exclude.includes(model.filenameBase)) {
3940
- task.log(
3941
- `Skipping - ${model.filenameBase}.${model.filenameExtension}\r`
3942
- );
3983
+ task.log(`Skipping - ${filename}\r`);
3943
3984
  resolve();
3944
3985
  return;
3945
3986
  }
@@ -3947,22 +3988,51 @@ var importFiles = (task) => mapSeries(
3947
3988
  resolve();
3948
3989
  return;
3949
3990
  }
3950
- const filepath = path.join(
3951
- task.downloadDir,
3952
- `${model.filenameBase}.${model.filenameExtension}`
3953
- );
3991
+ const filepath = path.join(task.downloadDir, `${filename}`);
3954
3992
  if (!existsSync(filepath)) {
3955
3993
  if (!model.nonstandard) {
3956
- task.log(
3957
- `Importing - ${model.filenameBase}.${model.filenameExtension} - No file found\r`
3958
- );
3994
+ task.log(`Importing - ${filename} - No file found\r`);
3959
3995
  }
3960
3996
  resolve();
3961
3997
  return;
3962
3998
  }
3963
- task.log(
3964
- `Importing - ${model.filenameBase}.${model.filenameExtension}\r`
3965
- );
3999
+ task.log(`Importing - ${filename}\r`);
4000
+ const columns = model.schema.filter((column) => column.name !== "id");
4001
+ const placeholder = columns.map(({ name }) => `@${name}`).join(", ");
4002
+ const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map((column) => column.name).join(", ")}) VALUES (${placeholder})`;
4003
+ const insert = db.prepare(prepareStatement);
4004
+ const insertLines = db.transaction((lines) => {
4005
+ for (const [rowNumber, line] of Object.entries(lines)) {
4006
+ try {
4007
+ if (task.prefix === void 0) {
4008
+ insert.run(line);
4009
+ } else {
4010
+ const prefixedLine = Object.fromEntries(
4011
+ Object.entries(
4012
+ line
4013
+ ).map(([columnName, value]) => [
4014
+ columnName,
4015
+ columns.find((col) => col.name === columnName)?.prefix ? `${task.prefix}${value}` : value
4016
+ ])
4017
+ );
4018
+ insert.run(prefixedLine);
4019
+ }
4020
+ } catch (error) {
4021
+ if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
4022
+ const primaryColumns = model.schema.filter(
4023
+ (column) => column.primary
4024
+ );
4025
+ task.logWarning(
4026
+ `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`
4027
+ );
4028
+ }
4029
+ task.logWarning(
4030
+ `Check ${filename} for invalid data on row ${rowNumber}.`
4031
+ );
4032
+ throw error;
4033
+ }
4034
+ }
4035
+ });
3966
4036
  if (model.filenameExtension === "txt") {
3967
4037
  const parser = parse({
3968
4038
  columns: true,
@@ -3971,23 +4041,21 @@ var importFiles = (task) => mapSeries(
3971
4041
  skip_empty_lines: true,
3972
4042
  ...task.csvOptions
3973
4043
  });
4044
+ let lines = [];
3974
4045
  parser.on("readable", () => {
3975
4046
  let record;
3976
4047
  while (record = parser.read()) {
3977
- try {
3978
- totalLineCount += 1;
3979
- lines.push(formatLine(record, model, totalLineCount));
3980
- if (lines.length >= maxInsertVariables / model.schema.length) {
3981
- importLines(task, lines, model, totalLineCount);
3982
- }
3983
- } catch (error) {
3984
- reject(error);
3985
- }
4048
+ totalLineCount += 1;
4049
+ lines.push(formatGtfsLine(record, model, totalLineCount));
3986
4050
  }
3987
4051
  });
3988
4052
  parser.on("end", () => {
3989
4053
  try {
3990
- importLines(task, lines, model, totalLineCount);
4054
+ insertLines(lines);
4055
+ task.log(
4056
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4057
+ true
4058
+ );
3991
4059
  } catch (error) {
3992
4060
  reject(error);
3993
4061
  }
@@ -3998,14 +4066,19 @@ var importFiles = (task) => mapSeries(
3998
4066
  } else if (model.filenameExtension === "geojson") {
3999
4067
  readFile2(filepath, "utf8").then((data) => {
4000
4068
  if (isValidJSON(data) === false) {
4001
- reject(
4002
- new Error(
4003
- `Invalid JSON in ${model.filenameBase}.${model.filenameExtension}`
4004
- )
4005
- );
4069
+ reject(new Error(`Invalid JSON in ${filename}`));
4006
4070
  }
4007
- const line = formatLine({ geojson: data }, model, totalLineCount);
4008
- importLines(task, [line], model, totalLineCount);
4071
+ totalLineCount += 1;
4072
+ const line = formatGtfsLine(
4073
+ { geojson: data },
4074
+ model,
4075
+ totalLineCount
4076
+ );
4077
+ insertLines([line]);
4078
+ task.log(
4079
+ `Importing - ${filename} - ${totalLineCount} lines imported\r`,
4080
+ true
4081
+ );
4009
4082
  resolve();
4010
4083
  }).catch(reject);
4011
4084
  } else {
@@ -4025,14 +4098,10 @@ async function importGtfs(initialConfig) {
4025
4098
  const db = openDb(config);
4026
4099
  const agencyCount = config.agencies.length;
4027
4100
  log2(
4028
- `Starting GTFS import for ${pluralize(
4029
- "file",
4030
- agencyCount,
4031
- true
4032
- )} using SQLite database at ${config.sqlitePath}`
4101
+ `Starting GTFS import for ${pluralize2("file", agencyCount, true)} using SQLite database at ${config.sqlitePath}`
4033
4102
  );
4034
- createTables(db);
4035
- await mapSeries(config.agencies, async (agency2) => {
4103
+ createGtfsTables(db);
4104
+ await mapSeries2(config.agencies, async (agency2) => {
4036
4105
  try {
4037
4106
  const tempPath = temporaryDirectory();
4038
4107
  const task = {
@@ -4057,100 +4126,50 @@ async function importGtfs(initialConfig) {
4057
4126
  logError: logError2
4058
4127
  };
4059
4128
  if (task.url) {
4060
- await downloadFiles(task);
4129
+ await downloadGtfsFiles(task);
4061
4130
  }
4062
- await readFiles(task);
4063
- await importFiles(task);
4064
- await updateRealtimeData(task);
4131
+ await extractGtfsFiles(task);
4132
+ await importGtfsFiles(db, task);
4133
+ await updateGtfsRealtimeData(task);
4065
4134
  await rm2(tempPath, { recursive: true });
4066
4135
  } catch (error) {
4067
- if (config.ignoreErrors) {
4068
- logError2(error.message);
4069
- } else {
4070
- throw error;
4071
- }
4136
+ handleImportError(error, config, logError2);
4072
4137
  }
4073
4138
  });
4139
+ log2(`Creating DB indexes`);
4140
+ createGtfsIndexes(db);
4074
4141
  log2(
4075
- `Completed GTFS import for ${pluralize("agency", agencyCount, true)}
4142
+ `Completed GTFS import for ${pluralize2("agency", agencyCount, true)}
4076
4143
  `
4077
4144
  );
4078
4145
  } catch (error) {
4079
- if (error?.code === "SQLITE_CANTOPEN") {
4080
- logError2(
4081
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
4082
- );
4083
- }
4146
+ handleDatabaseError2(error, config, logError2);
4147
+ }
4148
+ }
4149
+ function handleImportError(error, config, logError2) {
4150
+ if (config.ignoreErrors) {
4151
+ logError2(error.message);
4152
+ } else {
4084
4153
  throw error;
4085
4154
  }
4086
4155
  }
4087
- async function updateGtfsRealtime(initialConfig) {
4088
- const config = setDefaultConfig(initialConfig);
4089
- validateConfigForImport(config);
4090
- const log2 = log(config);
4091
- const logError2 = logError(config);
4092
- const logWarning2 = logWarning(config);
4093
- try {
4094
- openDb(config);
4095
- const agencyCount = config.agencies.length;
4096
- log2(
4097
- `Starting GTFS-Realtime refresh for ${pluralize(
4098
- "agencies",
4099
- agencyCount,
4100
- true
4101
- )} using SQLite database at ${config.sqlitePath}`
4156
+ function handleDatabaseError2(error, config, logError2) {
4157
+ if (error?.code === "SQLITE_CANTOPEN") {
4158
+ logError2(
4159
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
4102
4160
  );
4103
- deleteExpiredRealtimeData(config);
4104
- await mapSeries(config.agencies, async (agency2) => {
4105
- try {
4106
- const task = {
4107
- realtimeAlerts: agency2.realtimeAlerts,
4108
- realtimeTripUpdates: agency2.realtimeTripUpdates,
4109
- realtimeVehiclePositions: agency2.realtimeVehiclePositions,
4110
- downloadTimeout: config.downloadTimeout,
4111
- gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
4112
- ignoreErrors: config.ignoreErrors,
4113
- sqlitePath: config.sqlitePath,
4114
- currentTimestamp: Math.floor(Date.now() / 1e3),
4115
- log: log2,
4116
- logWarning: logWarning2,
4117
- logError: logError2
4118
- };
4119
- await updateRealtimeData(task);
4120
- } catch (error) {
4121
- if (config.ignoreErrors) {
4122
- logError2(error.message);
4123
- } else {
4124
- throw error;
4125
- }
4126
- }
4127
- });
4128
- log2(
4129
- `Completed GTFS-Realtime refresh for ${pluralize(
4130
- "agencies",
4131
- agencyCount,
4132
- true
4133
- )}
4134
- `
4135
- );
4136
- } catch (error) {
4137
- if (error?.code === "SQLITE_CANTOPEN") {
4138
- logError2(
4139
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
4140
- );
4141
- }
4142
- throw error;
4143
4161
  }
4162
+ throw error;
4144
4163
  }
4145
4164
 
4146
4165
  // src/lib/export.ts
4147
4166
  import path2 from "node:path";
4148
4167
  import { writeFile as writeFile2 } from "node:fs/promises";
4149
4168
  import { without, compact as compact2 } from "lodash-es";
4150
- import pluralize2 from "pluralize";
4169
+ import pluralize3 from "pluralize";
4151
4170
  import { stringify } from "csv-stringify";
4152
4171
  import sqlString3 from "sqlstring-sqlite";
4153
- import mapSeries2 from "promise-map-series";
4172
+ import mapSeries3 from "promise-map-series";
4154
4173
  import untildify4 from "untildify";
4155
4174
  var getAgencies = (db, config) => {
4156
4175
  try {
@@ -4183,7 +4202,7 @@ var exportGtfs = async (initialConfig) => {
4183
4202
  );
4184
4203
  }
4185
4204
  log2(
4186
- `Starting GTFS export for ${pluralize2(
4205
+ `Starting GTFS export for ${pluralize3(
4187
4206
  "agency",
4188
4207
  agencyCount,
4189
4208
  true
@@ -4196,7 +4215,7 @@ var exportGtfs = async (initialConfig) => {
4196
4215
  const modelsToExport = Object.values(models_exports).filter(
4197
4216
  (model) => model.extension !== "gtfs-realtime"
4198
4217
  );
4199
- const exportedFiles = await mapSeries2(
4218
+ const exportedFiles = await mapSeries3(
4200
4219
  modelsToExport,
4201
4220
  async (model) => {
4202
4221
  const filePath = path2.join(
@@ -4258,7 +4277,7 @@ var exportGtfs = async (initialConfig) => {
4258
4277
  return;
4259
4278
  }
4260
4279
  log2(`Completed GTFS export to ${exportPath}`);
4261
- log2(`Completed GTFS export for ${pluralize2("agency", agencyCount, true)}
4280
+ log2(`Completed GTFS export for ${pluralize3("agency", agencyCount, true)}
4262
4281
  `);
4263
4282
  };
4264
4283