gtfs 4.11.3 → 4.13.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.
Files changed (199) hide show
  1. package/README.md +27 -5
  2. package/dist/bin/gtfs-export.d.ts +1 -0
  3. package/dist/bin/gtfs-export.js +3646 -0
  4. package/dist/bin/gtfs-export.js.map +1 -0
  5. package/dist/bin/gtfs-import.d.ts +1 -0
  6. package/dist/bin/gtfs-import.js +4146 -0
  7. package/dist/bin/gtfs-import.js.map +1 -0
  8. package/dist/bin/gtfsrealtime-update.d.ts +1 -0
  9. package/dist/bin/gtfsrealtime-update.js +3802 -0
  10. package/dist/bin/gtfsrealtime-update.js.map +1 -0
  11. package/dist/index.d.ts +169 -0
  12. package/dist/index.js +5205 -0
  13. package/dist/index.js.map +1 -0
  14. package/package.json +27 -13
  15. package/.eslintrc.json +0 -28
  16. package/.github/workflows/nodejs.yml +0 -21
  17. package/.husky/pre-commit +0 -4
  18. package/@types/index.d.ts +0 -606
  19. package/@types/tests.ts +0 -31
  20. package/@types/tsconfig.json +0 -17
  21. package/CHANGELOG.md +0 -895
  22. package/bin/gtfs-export.js +0 -42
  23. package/bin/gtfs-import.js +0 -52
  24. package/bin/gtfsrealtime-update.js +0 -35
  25. package/config-sample-full.json +0 -20
  26. package/config-sample-rtupdates.json +0 -16
  27. package/config-sample.json +0 -8
  28. package/docs/images/node-gtfs-logo.svg +0 -18
  29. package/index.js +0 -1
  30. package/lib/advancedQuery.js +0 -36
  31. package/lib/db.js +0 -92
  32. package/lib/export.js +0 -141
  33. package/lib/file-utils.js +0 -103
  34. package/lib/geojson-utils.js +0 -138
  35. package/lib/gtfs/agencies.js +0 -32
  36. package/lib/gtfs/areas.js +0 -27
  37. package/lib/gtfs/attributions.js +0 -32
  38. package/lib/gtfs/booking-rules.js +0 -32
  39. package/lib/gtfs/calendar-dates.js +0 -32
  40. package/lib/gtfs/calendars.js +0 -32
  41. package/lib/gtfs/fare-attributes.js +0 -32
  42. package/lib/gtfs/fare-leg-rules.js +0 -32
  43. package/lib/gtfs/fare-media.js +0 -32
  44. package/lib/gtfs/fare-products.js +0 -32
  45. package/lib/gtfs/fare-rules.js +0 -32
  46. package/lib/gtfs/fare-transfer-rules.js +0 -32
  47. package/lib/gtfs/feed-info.js +0 -32
  48. package/lib/gtfs/frequencies.js +0 -32
  49. package/lib/gtfs/levels.js +0 -27
  50. package/lib/gtfs/location-group-stops.js +0 -32
  51. package/lib/gtfs/location-groups.js +0 -32
  52. package/lib/gtfs/locations.js +0 -32
  53. package/lib/gtfs/networks.js +0 -32
  54. package/lib/gtfs/pathways.js +0 -32
  55. package/lib/gtfs/route-networks.js +0 -32
  56. package/lib/gtfs/routes.js +0 -56
  57. package/lib/gtfs/shapes.js +0 -119
  58. package/lib/gtfs/stop-areas.js +0 -32
  59. package/lib/gtfs/stop-times.js +0 -32
  60. package/lib/gtfs/stops.js +0 -136
  61. package/lib/gtfs/timeframes.js +0 -32
  62. package/lib/gtfs/transfers.js +0 -32
  63. package/lib/gtfs/translations.js +0 -32
  64. package/lib/gtfs/trips.js +0 -27
  65. package/lib/gtfs-plus/calendar-attributes.js +0 -32
  66. package/lib/gtfs-plus/directions.js +0 -32
  67. package/lib/gtfs-plus/route-attributes.js +0 -32
  68. package/lib/gtfs-plus/stop-attributes.js +0 -32
  69. package/lib/gtfs-realtime/service-alerts.js +0 -34
  70. package/lib/gtfs-realtime/stop-time-updates.js +0 -32
  71. package/lib/gtfs-realtime/trip-updates.js +0 -32
  72. package/lib/gtfs-realtime/vehicle-positions.js +0 -32
  73. package/lib/gtfs-ride/board-alights.js +0 -32
  74. package/lib/gtfs-ride/ride-feed-infos.js +0 -32
  75. package/lib/gtfs-ride/rider-trips.js +0 -32
  76. package/lib/gtfs-ride/riderships.js +0 -32
  77. package/lib/gtfs-ride/trip-capacities.js +0 -32
  78. package/lib/gtfs.js +0 -261
  79. package/lib/import.js +0 -803
  80. package/lib/log-utils.js +0 -73
  81. package/lib/non-standard/timetable-notes-references.js +0 -32
  82. package/lib/non-standard/timetable-notes.js +0 -32
  83. package/lib/non-standard/timetable-pages.js +0 -32
  84. package/lib/non-standard/timetable-stop-order.js +0 -32
  85. package/lib/non-standard/timetables.js +0 -32
  86. package/lib/non-standard/trips-dated-vehicle-journey.js +0 -32
  87. package/lib/ods/deadhead-times.js +0 -32
  88. package/lib/ods/deadheads.js +0 -32
  89. package/lib/ods/ops-locations.js +0 -32
  90. package/lib/ods/run-events.js +0 -32
  91. package/lib/ods/runs-pieces.js +0 -32
  92. package/lib/utils.js +0 -178
  93. package/models/gtfs/agency.js +0 -49
  94. package/models/gtfs/areas.js +0 -19
  95. package/models/gtfs/attributions.js +0 -68
  96. package/models/gtfs/booking-rules.js +0 -92
  97. package/models/gtfs/calendar-dates.js +0 -34
  98. package/models/gtfs/calendar.js +0 -76
  99. package/models/gtfs/fare-attributes.js +0 -48
  100. package/models/gtfs/fare-leg-rules.js +0 -55
  101. package/models/gtfs/fare-media.js +0 -26
  102. package/models/gtfs/fare-products.js +0 -35
  103. package/models/gtfs/fare-rules.js +0 -34
  104. package/models/gtfs/fare-transfer-rules.js +0 -56
  105. package/models/gtfs/feed-info.js +0 -50
  106. package/models/gtfs/frequencies.js +0 -46
  107. package/models/gtfs/levels.js +0 -25
  108. package/models/gtfs/location-group-stops.js +0 -22
  109. package/models/gtfs/location-groups.js +0 -19
  110. package/models/gtfs/locations.js +0 -12
  111. package/models/gtfs/networks.js +0 -20
  112. package/models/gtfs/pathways.js +0 -74
  113. package/models/gtfs/route-networks.js +0 -21
  114. package/models/gtfs/routes.js +0 -79
  115. package/models/gtfs/shapes.js +0 -41
  116. package/models/gtfs/stop-areas.js +0 -20
  117. package/models/gtfs/stop-times.js +0 -120
  118. package/models/gtfs/stops.js +0 -85
  119. package/models/gtfs/timeframes.js +0 -29
  120. package/models/gtfs/transfers.js +0 -56
  121. package/models/gtfs/translations.js +0 -48
  122. package/models/gtfs/trips.js +0 -70
  123. package/models/gtfs-plus/calendar-attributes.js +0 -22
  124. package/models/gtfs-plus/directions.js +0 -29
  125. package/models/gtfs-plus/route-attributes.js +0 -34
  126. package/models/gtfs-plus/stop-attributes.js +0 -35
  127. package/models/gtfs-realtime/service-alert-targets.js +0 -37
  128. package/models/gtfs-realtime/service-alerts.js +0 -60
  129. package/models/gtfs-realtime/stop-time-updates.js +0 -85
  130. package/models/gtfs-realtime/trip-updates.js +0 -75
  131. package/models/gtfs-realtime/vehicle-positions.js +0 -135
  132. package/models/gtfs-ride/board-alight.js +0 -132
  133. package/models/gtfs-ride/ride-feed-info.js +0 -40
  134. package/models/gtfs-ride/rider-trip.js +0 -113
  135. package/models/gtfs-ride/ridership.js +0 -127
  136. package/models/gtfs-ride/trip-capacity.js +0 -51
  137. package/models/models.js +0 -120
  138. package/models/non-standard/timetable-notes-references.js +0 -50
  139. package/models/non-standard/timetable-notes.js +0 -24
  140. package/models/non-standard/timetable-pages.js +0 -23
  141. package/models/non-standard/timetable-stop-order.js +0 -32
  142. package/models/non-standard/timetables.js +0 -144
  143. package/models/non-standard/trips-dated-vehicle-journey.js +0 -34
  144. package/models/ods/deadhead-times.js +0 -65
  145. package/models/ods/deadheads.js +0 -60
  146. package/models/ods/ops-locations.js +0 -46
  147. package/models/ods/run-events.js +0 -70
  148. package/models/ods/runs-pieces.js +0 -59
  149. package/test/fixture/caltrain_20160406.zip +0 -0
  150. package/test/mocha/advanced-query.js +0 -74
  151. package/test/mocha/delete-db.js +0 -62
  152. package/test/mocha/export-gtfs.js +0 -147
  153. package/test/mocha/fare-transfer-rules.js +0 -32
  154. package/test/mocha/get-agencies.js +0 -90
  155. package/test/mocha/get-areas.js +0 -27
  156. package/test/mocha/get-attributions.js +0 -27
  157. package/test/mocha/get-board-alights.js +0 -28
  158. package/test/mocha/get-booking-rules.js +0 -28
  159. package/test/mocha/get-calendar-attributes.js +0 -33
  160. package/test/mocha/get-calendar-dates.js +0 -107
  161. package/test/mocha/get-calendars.js +0 -94
  162. package/test/mocha/get-directions.js +0 -28
  163. package/test/mocha/get-fare-attributes.js +0 -51
  164. package/test/mocha/get-fare-leg-rules.js +0 -27
  165. package/test/mocha/get-fare-media.js +0 -27
  166. package/test/mocha/get-fare-products.js +0 -27
  167. package/test/mocha/get-fare-rules.js +0 -50
  168. package/test/mocha/get-feed-info.js +0 -28
  169. package/test/mocha/get-frequencies.js +0 -28
  170. package/test/mocha/get-levels.js +0 -28
  171. package/test/mocha/get-location-group-stops.js +0 -33
  172. package/test/mocha/get-location-groups.js +0 -28
  173. package/test/mocha/get-locations.js +0 -69
  174. package/test/mocha/get-networks.js +0 -28
  175. package/test/mocha/get-pathways.js +0 -28
  176. package/test/mocha/get-ride-feed-infos.js +0 -24
  177. package/test/mocha/get-rider-trips.js +0 -28
  178. package/test/mocha/get-riderships.js +0 -28
  179. package/test/mocha/get-route-attributes.js +0 -33
  180. package/test/mocha/get-route-networks.js +0 -28
  181. package/test/mocha/get-routes.js +0 -143
  182. package/test/mocha/get-shapes-as-geojson.js +0 -92
  183. package/test/mocha/get-shapes.js +0 -240
  184. package/test/mocha/get-stop-attributes.js +0 -28
  185. package/test/mocha/get-stops-as-geojson.js +0 -87
  186. package/test/mocha/get-stops.js +0 -343
  187. package/test/mocha/get-stoptimes.js +0 -67
  188. package/test/mocha/get-timeframes.js +0 -28
  189. package/test/mocha/get-timetable-pages.js +0 -28
  190. package/test/mocha/get-timetable-stop-orders.js +0 -33
  191. package/test/mocha/get-timetables.js +0 -28
  192. package/test/mocha/get-transfers.js +0 -28
  193. package/test/mocha/get-translations.js +0 -28
  194. package/test/mocha/get-trip-capacities.js +0 -28
  195. package/test/mocha/get-trips.js +0 -53
  196. package/test/mocha/import-gtfs.js +0 -173
  197. package/test/mocha/open-db.js +0 -149
  198. package/test/mocha/raw-query.js +0 -34
  199. package/test/test-config.js +0 -12
package/lib/import.js DELETED
@@ -1,803 +0,0 @@
1
- import path from 'node:path';
2
- import { createReadStream, existsSync, lstatSync } from 'node:fs';
3
- import { cp, readdir, rename, readFile, writeFile } from 'node:fs/promises';
4
- import fetch from 'node-fetch';
5
- import { parse } from 'csv-parse';
6
- import pluralize from 'pluralize';
7
- import stripBomStream from 'strip-bom-stream';
8
- import { dir } from 'tmp-promise';
9
- import untildify from 'untildify';
10
- import mapSeries from 'promise-map-series';
11
- import GtfsRealtimeBindings from 'gtfs-realtime-bindings';
12
- import sqlString from 'sqlstring-sqlite';
13
-
14
- import models from '../models/models.js';
15
- import { openDb } from './db.js';
16
- import { unzip } from './file-utils.js';
17
- import { isValidJSON } from './geojson-utils.js';
18
- import {
19
- log as _log,
20
- logError as _logError,
21
- logWarning as _logWarning,
22
- } from './log-utils.js';
23
- import {
24
- calculateSecondsFromMidnight,
25
- setDefaultConfig,
26
- validateConfigForImport,
27
- convertLongTimeToDate,
28
- padLeadingZeros,
29
- } from './utils.js';
30
-
31
- const downloadFiles = async (task) => {
32
- task.log(`Downloading GTFS from ${task.agency_url}`);
33
-
34
- task.path = `${task.downloadDir}/gtfs.zip`;
35
-
36
- const response = await fetch(task.agency_url, {
37
- method: 'GET',
38
- headers: task.headers || {},
39
- signal: task.downloadTimeout
40
- ? AbortSignal.timeout(task.downloadTimeout)
41
- : undefined,
42
- });
43
-
44
- if (response.status !== 200) {
45
- throw new Error(`Unable to download GTFS from ${task.agency_url}`);
46
- }
47
-
48
- const buffer = await response.arrayBuffer();
49
-
50
- await writeFile(task.path, Buffer.from(buffer));
51
- task.log('Download successful');
52
- };
53
-
54
- const downloadGtfsRealtimeData = async (url, task) => {
55
- const response = await fetch(url, {
56
- method: 'GET',
57
- headers: {
58
- ...{},
59
- ...task.realtime_headers,
60
- ...{ 'Accept-Encoding': 'gzip' },
61
- },
62
- });
63
-
64
- if (response.status !== 200) {
65
- task.warn(`Unable to download GTFS-Realtime from ${url}`);
66
- return null;
67
- }
68
-
69
- const buffer = await response.arrayBuffer();
70
- const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
71
- new Uint8Array(buffer),
72
- );
73
- return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
74
- enums: String,
75
- longs: String,
76
- bytes: String,
77
- defaults: true,
78
- arrays: true,
79
- objects: true,
80
- oneofs: true,
81
- });
82
- };
83
-
84
- function getDescendantProp(obj, desc, defaultvalue) {
85
- if (desc === undefined) return defaultvalue;
86
- const arr = desc.split('.');
87
- while (arr.length) {
88
- const nextKey = arr.shift();
89
- if (obj == null) {
90
- return defaultvalue;
91
- } else if (nextKey.includes('[')) {
92
- const arrayKey = nextKey.match(/(\w*)\[(\d+)\]/);
93
- if (obj[arrayKey[1]] === undefined) {
94
- return defaultvalue;
95
- }
96
-
97
- if (obj[arrayKey[1]][arrayKey[2]] === undefined) {
98
- return defaultvalue;
99
- }
100
-
101
- obj = obj[arrayKey[1]][arrayKey[2]];
102
- } else {
103
- if (obj[nextKey] === undefined) {
104
- return defaultvalue;
105
- }
106
- obj = obj[nextKey];
107
- }
108
- }
109
-
110
- if (obj.__isLong__) return convertLongTimeToDate(obj);
111
-
112
- return obj;
113
- }
114
-
115
- const markRealtimeDataStale = (config) => {
116
- const log = _log(config);
117
- const logError = _logError(config);
118
-
119
- try {
120
- const db = openDb(config);
121
-
122
- log(`Marking GTFS-Realtime data as stale`);
123
- db.prepare(`UPDATE vehicle_positions SET is_updated=0`).run();
124
- db.prepare(`UPDATE trip_updates SET is_updated=0`).run();
125
- db.prepare(`UPDATE stop_time_updates SET is_updated=0`).run();
126
- db.prepare(`UPDATE service_alerts SET is_updated=0`).run();
127
- db.prepare(`UPDATE service_alert_targets SET is_updated=0`).run();
128
- log(`Marked GTFS-Realtime data as stale\r`, true);
129
- } catch (error) {
130
- if (
131
- error instanceof Error &&
132
- error.code === 'SQLITE_ERROR' &&
133
- error.message?.startsWith('no such table')
134
- ) {
135
- logError(
136
- 'Run `gtfs-import` before running the `gtfsrealtime-update` command to set up tables.',
137
- );
138
- throw error;
139
- }
140
- }
141
- };
142
-
143
- const cleanStaleRealtimeData = (config) => {
144
- const log = _log(config);
145
- const db = openDb(config);
146
-
147
- log(`Cleaning stale GTFS-RT data`);
148
- db.prepare(`DELETE FROM vehicle_positions WHERE is_updated=0`).run();
149
- db.prepare(`DELETE FROM trip_updates WHERE is_updated=0`).run();
150
- db.prepare(`DELETE FROM stop_time_updates WHERE is_updated=0`).run();
151
- db.prepare(`DELETE FROM service_alerts WHERE is_updated=0`).run();
152
- db.prepare(`DELETE FROM service_alert_targets WHERE is_updated=0`).run();
153
- log(`Cleaned stale GTFS-Realtime data\r`, true);
154
- };
155
-
156
- const updateRealtimeData = async (task) => {
157
- const db = openDb(task);
158
-
159
- const model = {
160
- vehicle_positions: models.find(
161
- (x) => x.filenameBase === 'vehicle_positions',
162
- ),
163
- trip_updates: models.find((x) => x.filenameBase === 'trip_updates'),
164
- stop_time_updates: models.find(
165
- (x) => x.filenameBase === 'stop_time_updates',
166
- ),
167
- service_alerts: models.find((x) => x.filenameBase === 'service_alerts'),
168
- service_alert_targets: models.find(
169
- (x) => x.filenameBase === 'service_alert_targets',
170
- ),
171
- };
172
-
173
- const fields = {
174
- vehicle_positions: model.vehicle_positions.schema
175
- .map((column) => column.name)
176
- .join(', '),
177
- trip_updates: model.trip_updates.schema
178
- .map((column) => column.name)
179
- .join(', '),
180
- stop_time_updates: model.stop_time_updates.schema
181
- .map((column) => column.name)
182
- .join(', '),
183
- service_alerts: model.service_alerts.schema
184
- .map((column) => column.name)
185
- .join(', '),
186
- service_alert_targets: model.service_alert_targets.schema
187
- .map((column) => column.name)
188
- .join(', '),
189
- };
190
-
191
- task.log(
192
- `Starting GTFS-Realtime import from ${task.realtime_urls.length} urls`,
193
- );
194
-
195
- for (const realtimeUrl of task.realtime_urls) {
196
- task.log(`Downloading GTFS-Realtime from ${realtimeUrl}`);
197
- // eslint-disable-next-line no-await-in-loop
198
- const gtfsRealtimeData = await downloadGtfsRealtimeData(realtimeUrl, task);
199
-
200
- if (!gtfsRealtimeData?.entity) {
201
- continue;
202
- }
203
-
204
- task.log(`Download successful`);
205
-
206
- let totalLineCount = 0;
207
-
208
- for (const entity of gtfsRealtimeData.entity) {
209
- // Determine the type of GTFS-Realtime
210
- let gtfsRealtimeType = null;
211
- if (entity.vehicle) {
212
- gtfsRealtimeType = 'vehicle_positions';
213
- }
214
-
215
- if (entity.tripUpdate) {
216
- gtfsRealtimeType = 'trip_updates';
217
- }
218
-
219
- if (entity.alert) {
220
- gtfsRealtimeType = 'service_alerts';
221
- }
222
-
223
- if (!gtfsRealtimeType) {
224
- break;
225
- }
226
-
227
- // Do base processing
228
- const fieldValues = model[gtfsRealtimeType].schema.map((column) =>
229
- sqlString.escape(
230
- getDescendantProp(entity, column.source, column.default),
231
- ),
232
- );
233
-
234
- try {
235
- db.prepare(
236
- `REPLACE INTO ${model[gtfsRealtimeType].filenameBase} (${
237
- fields[gtfsRealtimeType]
238
- }) VALUES (${fieldValues.join(', ')})`,
239
- ).run();
240
- } catch (error) {
241
- task.warn('Import error: ' + error.message);
242
- }
243
-
244
- // Special processing for tripUpdates
245
- if (entity.tripUpdate) {
246
- const stopTimeUpdateArray = [];
247
- for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
248
- stopTimeUpdate.parent = entity;
249
- const subValues = model.stop_time_updates.schema.map((column) =>
250
- sqlString.escape(
251
- getDescendantProp(stopTimeUpdate, column.source, column.default),
252
- ),
253
- );
254
- stopTimeUpdateArray.push(`(${subValues.join(', ')})`);
255
- totalLineCount++;
256
- }
257
-
258
- try {
259
- db.prepare(
260
- `REPLACE INTO ${model.stop_time_updates.filenameBase} (${
261
- fields.stop_time_updates
262
- }) VALUES ${stopTimeUpdateArray.join(', ')}`,
263
- ).run();
264
- } catch (error) {
265
- task.warn('Import error: ' + error.message);
266
- }
267
- }
268
-
269
- // Special processing for serviceAlerts
270
- if (entity.alert) {
271
- const alertTargetArray = [];
272
- for (const informedEntity of entity.alert.informedEntity) {
273
- informedEntity.parent = entity;
274
- const subValues = model.service_alert_targets.schema.map((column) =>
275
- sqlString.escape(
276
- getDescendantProp(informedEntity, column.source, column.default),
277
- ),
278
- );
279
- alertTargetArray.push(`(${subValues.join(', ')})`);
280
- totalLineCount++;
281
- }
282
-
283
- try {
284
- db.prepare(
285
- `REPLACE INTO ${model.service_alert_targets.filenameBase} (${
286
- fields.service_alert_targets
287
- }) VALUES ${alertTargetArray.join(', ')}`,
288
- ).run();
289
- } catch (error) {
290
- task.warn('Import error: ' + error.message);
291
- }
292
- }
293
-
294
- task.log(`Importing - ${totalLineCount++} entries imported\r`, true);
295
- }
296
- }
297
-
298
- task.log(`GTFS-Realtime data import complete`);
299
- };
300
-
301
- const getTextFiles = async (folderPath) => {
302
- const files = await readdir(folderPath);
303
- return files.filter((filename) => filename.slice(-3) === 'txt');
304
- };
305
-
306
- const readFiles = async (task) => {
307
- const gtfsPath = untildify(task.path);
308
- task.log(`Importing GTFS from ${task.path}\r`);
309
- if (path.extname(gtfsPath) === '.zip') {
310
- try {
311
- await unzip(gtfsPath, task.downloadDir);
312
- const textFiles = await getTextFiles(task.downloadDir);
313
-
314
- // If no .txt files in this directory, check for subdirectories and copy them here
315
- if (textFiles.length === 0) {
316
- const files = await readdir(task.downloadDir);
317
- // Ignore system directories within zip file
318
- const folders = files
319
- .filter((filename) => !['__MACOSX'].includes(filename))
320
- .map((filename) => path.join(task.downloadDir, filename))
321
- .filter((source) => lstatSync(source).isDirectory());
322
-
323
- if (folders.length > 1) {
324
- throw new Error(
325
- `More than one subfolder found in zip file at \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`,
326
- );
327
- } else if (folders.length === 0) {
328
- throw new Error(
329
- `No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`,
330
- );
331
- }
332
-
333
- const subfolderName = folders[0];
334
- const directoryTextFiles = await getTextFiles(subfolderName);
335
-
336
- if (directoryTextFiles.length === 0) {
337
- throw new Error(
338
- `No .txt files found in \`${task.path}\`. Ensure that .txt files are in the top level of the zip file, or in a single subdirectory.`,
339
- );
340
- }
341
-
342
- await Promise.all(
343
- directoryTextFiles.map(async (fileName) =>
344
- rename(
345
- path.join(subfolderName, fileName),
346
- path.join(task.downloadDir, fileName),
347
- ),
348
- ),
349
- );
350
- }
351
- } catch (error) {
352
- task.error(error);
353
- throw new Error(`Unable to unzip file ${task.path}`);
354
- }
355
- } else {
356
- // Local file is unzipped, just copy it from there.
357
- try {
358
- await cp(gtfsPath, task.downloadDir, { recursive: true });
359
- } catch {
360
- throw new Error(
361
- `Unable to load files from path \`${gtfsPath}\` defined in configuration. Verify that path exists and contains GTFS files.`,
362
- );
363
- }
364
- }
365
- };
366
-
367
- const createTables = (db) => {
368
- for (const model of models) {
369
- if (!model.schema) {
370
- return;
371
- }
372
-
373
- const columns = model.schema.map((column) => {
374
- let check = '';
375
- if (column.min !== undefined && column.max) {
376
- check = `CHECK( ${column.name} >= ${column.min} AND ${column.name} <= ${column.max} )`;
377
- } else if (column.min) {
378
- check = `CHECK( ${column.name} >= ${column.min} )`;
379
- } else if (column.max) {
380
- check = `CHECK( ${column.name} <= ${column.max} )`;
381
- }
382
-
383
- const required = column.required ? 'NOT NULL' : '';
384
- const columnDefault = column.default ? 'DEFAULT ' + column.default : '';
385
- const columnCollation = column.nocase ? 'COLLATE NOCASE' : '';
386
- return `${column.name} ${column.type} ${check} ${required} ${columnDefault} ${columnCollation}`;
387
- });
388
-
389
- // Find Primary Key fields
390
- const primaryColumns = model.schema.filter((column) => column.primary);
391
-
392
- if (primaryColumns.length > 0) {
393
- columns.push(
394
- `PRIMARY KEY (${primaryColumns
395
- .map((column) => column.name)
396
- .join(', ')})`,
397
- );
398
- }
399
-
400
- db.prepare(`DROP TABLE IF EXISTS ${model.filenameBase};`).run();
401
-
402
- db.prepare(
403
- `CREATE TABLE ${model.filenameBase} (${columns.join(', ')});`,
404
- ).run();
405
-
406
- for (const column of model.schema.filter((column) => column.index)) {
407
- const unique = column.index === 'unique' ? 'UNIQUE' : '';
408
- db.prepare(
409
- `CREATE ${unique} INDEX idx_${model.filenameBase}_${column.name} ON ${model.filenameBase} (${column.name});`,
410
- ).run();
411
- }
412
- }
413
- };
414
-
415
- const formatLine = (line, model, totalLineCount) => {
416
- const lineNumber = totalLineCount + 1;
417
-
418
- const formattedLine = {};
419
-
420
- for (const columnSchema of model.schema) {
421
- const lineValue = line[columnSchema.name];
422
-
423
- if (columnSchema.type === 'integer') {
424
- // Convert fields that should be integer
425
- formattedLine[columnSchema.name] = Number.parseInt(lineValue, 10);
426
- } else if (columnSchema.type === 'real') {
427
- // Convert fields that should be float
428
- formattedLine[columnSchema.name] = Number.parseFloat(lineValue);
429
- } else {
430
- formattedLine[columnSchema.name] = lineValue;
431
- }
432
-
433
- if (
434
- formattedLine[columnSchema.name] === '' ||
435
- formattedLine[columnSchema.name] === undefined ||
436
- formattedLine[columnSchema.name] === null ||
437
- Number.isNaN(formattedLine[columnSchema.name])
438
- ) {
439
- // Add null values
440
- formattedLine[columnSchema.name] = null;
441
- }
442
-
443
- // Validate required
444
- if (
445
- columnSchema.required === true &&
446
- formattedLine[columnSchema.name] === null
447
- ) {
448
- throw new Error(
449
- `Missing required value in ${model.filenameBase}.${model.filenameExtension} for ${columnSchema.name} on line ${lineNumber}.`,
450
- );
451
- }
452
-
453
- // Validate minimum
454
- if (
455
- columnSchema.min !== undefined &&
456
- formattedLine[columnSchema.name] < columnSchema.min
457
- ) {
458
- throw new Error(
459
- `Invalid value in ${model.filenameBase}.${model.filenameExtension} for ${columnSchema.name} on line ${lineNumber}: below minimum value of ${columnSchema.min}.`,
460
- );
461
- }
462
-
463
- // Validate maximum
464
- if (
465
- columnSchema.max !== undefined &&
466
- formattedLine[columnSchema.name] > columnSchema.max
467
- ) {
468
- throw new Error(
469
- `Invalid value in ${model.filenameBase}.${model.filenameExtension} for ${columnSchema.name} on line ${lineNumber}: above maximum value of ${columnSchema.max}.`,
470
- );
471
- }
472
- }
473
-
474
- // Convert to midnight timestamp and add timestamp columns as integer seconds from midnight
475
- const timeColumnNames = [
476
- 'start_time',
477
- 'end_time',
478
- 'arrival_time',
479
- 'departure_time',
480
- 'prior_notice_last_time',
481
- 'prior_notice_start_time',
482
- 'start_pickup_drop_off_window',
483
- ];
484
-
485
- for (const timeColumnName of timeColumnNames) {
486
- if (formattedLine[timeColumnName]) {
487
- const timestampColumnName = timeColumnName.endsWith('time')
488
- ? `${timeColumnName}stamp`
489
- : `${timeColumnName}_timestamp`;
490
- formattedLine[timestampColumnName] = calculateSecondsFromMidnight(
491
- formattedLine[timeColumnName],
492
- );
493
-
494
- // Ensure leading zeros for time columns
495
- formattedLine[timeColumnName] = padLeadingZeros(
496
- formattedLine[timeColumnName],
497
- );
498
- }
499
- }
500
-
501
- return formattedLine;
502
- };
503
-
504
- const importLines = (task, lines, model, totalLineCount) => {
505
- const db = openDb(task);
506
-
507
- if (lines.length === 0) {
508
- return;
509
- }
510
-
511
- const linesToImportCount = lines.length;
512
- const columns = model.schema.filter((column) => column.name !== 'id');
513
- const placeholders = [];
514
- const values = [];
515
-
516
- while (lines.length > 0) {
517
- const line = lines.pop();
518
- placeholders.push(`(${columns.map(() => '?').join(', ')})`);
519
- values.push(
520
- ...columns.map((column) => {
521
- if (task.prefix !== undefined && column.prefix === true) {
522
- // Add prefixes to field values if needed
523
- return `${task.prefix}${line[column.name]}`;
524
- }
525
-
526
- return line[column.name];
527
- }),
528
- );
529
- }
530
-
531
- try {
532
- db.prepare(
533
- `INSERT ${task.ignoreDuplicates ? 'OR IGNORE' : ''} INTO ${
534
- model.filenameBase
535
- } (${columns
536
- .map((column) => column.name)
537
- .join(', ')}) VALUES ${placeholders.join(',')}`,
538
- ).run(...values);
539
- } catch (error) {
540
- if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
541
- const primaryColumns = model.schema.filter((column) => column.primary);
542
- task.warn(
543
- `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`,
544
- );
545
- }
546
-
547
- task.warn(
548
- `Check ${model.filenameBase}.${model.filenameExtension} for invalid data between lines ${
549
- totalLineCount - linesToImportCount
550
- } and ${totalLineCount}.`,
551
- );
552
- throw error;
553
- }
554
-
555
- task.log(
556
- `Importing - ${model.filenameBase}.${model.filenameExtension} - ${totalLineCount} lines imported\r`,
557
- true,
558
- );
559
- };
560
-
561
- const importFiles = (task) =>
562
- mapSeries(
563
- models,
564
- (model) =>
565
- new Promise((resolve, reject) => {
566
- const lines = [];
567
- let totalLineCount = 0;
568
- const maxInsertVariables = 32_000;
569
-
570
- // Loop through each GTFS file
571
- // Filter out excluded files from config
572
- if (task.exclude && task.exclude.includes(model.filenameBase)) {
573
- task.log(
574
- `Skipping - ${model.filenameBase}.${model.filenameExtension}\r`,
575
- );
576
- resolve();
577
- return;
578
- }
579
-
580
- // If the model is a database/gtfs-realtime model then silently exit
581
- if (model.extension === 'gtfs-realtime') {
582
- resolve();
583
- return;
584
- }
585
-
586
- const filepath = path.join(
587
- task.downloadDir,
588
- `${model.filenameBase}.${model.filenameExtension}`,
589
- );
590
-
591
- if (!existsSync(filepath)) {
592
- // Log only missing standard GTFS files
593
- if (!model.nonstandard) {
594
- task.log(
595
- `Importing - ${model.filenameBase}.${model.filenameExtension} - No file found\r`,
596
- );
597
- }
598
-
599
- resolve();
600
- return;
601
- }
602
-
603
- task.log(
604
- `Importing - ${model.filenameBase}.${model.filenameExtension}\r`,
605
- );
606
-
607
- if (model.filenameExtension === 'txt') {
608
- const parser = parse({
609
- columns: true,
610
- relax_quotes: true,
611
- trim: true,
612
- skip_empty_lines: true,
613
- ...task.csvOptions,
614
- });
615
-
616
- parser.on('readable', () => {
617
- let record;
618
-
619
- while ((record = parser.read())) {
620
- try {
621
- totalLineCount += 1;
622
- lines.push(formatLine(record, model, totalLineCount));
623
- // If we have a bunch of lines ready to insert, then do it
624
- if (lines.length >= maxInsertVariables / model.schema.length) {
625
- importLines(task, lines, model, totalLineCount);
626
- }
627
- } catch (error) {
628
- reject(error);
629
- }
630
- }
631
- });
632
-
633
- parser.on('end', () => {
634
- try {
635
- // Insert all remaining lines
636
- importLines(task, lines, model, totalLineCount);
637
- } catch (error) {
638
- reject(error);
639
- }
640
- resolve();
641
- });
642
-
643
- parser.on('error', reject);
644
-
645
- createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
646
- } else if (model.filenameExtension === 'geojson') {
647
- readFile(filepath, 'utf8')
648
- .then((data) => {
649
- if (isValidJSON(data) === false) {
650
- reject(
651
- new Error(
652
- `Invalid JSON in ${model.filenameBase}.${model.filenameExtension}`,
653
- ),
654
- );
655
- }
656
- const line = formatLine({ geojson: data }, model, totalLineCount);
657
- importLines(task, [line], model, totalLineCount);
658
- resolve();
659
- })
660
- .catch(reject);
661
- } else {
662
- reject(
663
- new Error(`Unsupported file type: ${model.filenameExtension}`),
664
- );
665
- }
666
- }),
667
- );
668
-
669
- export async function importGtfs(initialConfig) {
670
- const config = setDefaultConfig(initialConfig);
671
- validateConfigForImport(config);
672
- const log = _log(config);
673
- const logError = _logError(config);
674
- const logWarning = _logWarning(config);
675
- try {
676
- const db = openDb(config);
677
-
678
- const agencyCount = config.agencies.length;
679
- log(
680
- `Starting GTFS import for ${pluralize(
681
- 'file',
682
- agencyCount,
683
- true,
684
- )} using SQLite database at ${config.sqlitePath}`,
685
- );
686
-
687
- createTables(db);
688
-
689
- await mapSeries(config.agencies, async (agency) => {
690
- const { path, cleanup } = await dir({ unsafeCleanup: true });
691
-
692
- const task = {
693
- exclude: agency.exclude,
694
- agency_url: agency.url,
695
- headers: agency.headers || false,
696
- realtime_headers: agency.realtimeHeaders || false,
697
- realtime_urls: agency.realtimeUrls || false,
698
- downloadDir: path,
699
- downloadTimeout: config.downloadTimeout,
700
- path: agency.path,
701
- csvOptions: config.csvOptions || {},
702
- ignoreDuplicates: config.ignoreDuplicates,
703
- sqlitePath: config.sqlitePath,
704
- prefix: agency.prefix,
705
- log,
706
- warn: logWarning,
707
- error: logError,
708
- };
709
-
710
- try {
711
- if (task.agency_url) {
712
- await downloadFiles(task);
713
- }
714
-
715
- await readFiles(task);
716
- await importFiles(task);
717
-
718
- if (task.realtime_urls) {
719
- await updateRealtimeData(task);
720
- }
721
-
722
- cleanup();
723
- } catch (error) {
724
- if (config.ignoreErrors) {
725
- logError(error.message);
726
- } else {
727
- throw error;
728
- }
729
- }
730
- });
731
-
732
- log(
733
- `Completed GTFS import for ${pluralize('agency', agencyCount, true)}\n`,
734
- );
735
- } catch (error) {
736
- if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
737
- logError(
738
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
739
- );
740
- }
741
-
742
- throw error;
743
- }
744
- }
745
-
746
- export async function updateGtfsRealtime(initialConfig) {
747
- const config = setDefaultConfig(initialConfig);
748
- validateConfigForImport(config);
749
- const log = _log(config);
750
- const logError = _logError(config);
751
- const logWarning = _logWarning(config);
752
-
753
- try {
754
- openDb(config);
755
-
756
- const agencyCount = config.agencies.length;
757
- log(
758
- `Starting GTFS-Realtime refresh for ${pluralize(
759
- 'agencies',
760
- agencyCount,
761
- true,
762
- )} using SQLite database at ${config.sqlitePath}`,
763
- );
764
-
765
- markRealtimeDataStale(config);
766
-
767
- await Promise.all(
768
- config.agencies.map(async (agency) => {
769
- if (!agency.realtimeUrls) {
770
- return;
771
- }
772
-
773
- const task = {
774
- realtime_headers: agency.realtimeHeaders || false,
775
- realtime_urls: agency.realtimeUrls || false,
776
- sqlitePath: config.sqlitePath,
777
- log,
778
- warn: logWarning,
779
- error: logError,
780
- };
781
-
782
- await updateRealtimeData(task);
783
- }),
784
- );
785
-
786
- cleanStaleRealtimeData(config);
787
- log(
788
- `Completed GTFS-Realtime refresh for ${pluralize(
789
- 'agencies',
790
- agencyCount,
791
- true,
792
- )}\n`,
793
- );
794
- } catch (error) {
795
- if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
796
- logError(
797
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
798
- );
799
- }
800
-
801
- throw error;
802
- }
803
- }