gtfs 4.12.0 → 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.
- package/README.md +27 -5
- package/dist/bin/gtfs-export.d.ts +1 -0
- package/dist/bin/gtfs-export.js +3646 -0
- package/dist/bin/gtfs-export.js.map +1 -0
- package/dist/bin/gtfs-import.d.ts +1 -0
- package/dist/bin/gtfs-import.js +4146 -0
- package/dist/bin/gtfs-import.js.map +1 -0
- package/dist/bin/gtfsrealtime-update.d.ts +1 -0
- package/dist/bin/gtfsrealtime-update.js +3802 -0
- package/dist/bin/gtfsrealtime-update.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.js +5205 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -12
- package/.eslintrc.json +0 -28
- package/.github/workflows/nodejs.yml +0 -21
- package/.husky/pre-commit +0 -4
- package/@types/index.d.ts +0 -606
- package/@types/tests.ts +0 -31
- package/@types/tsconfig.json +0 -17
- package/CHANGELOG.md +0 -905
- package/bin/gtfs-export.js +0 -42
- package/bin/gtfs-import.js +0 -52
- package/bin/gtfsrealtime-update.js +0 -35
- package/config-sample-full.json +0 -20
- package/config-sample-rtupdates.json +0 -16
- package/config-sample.json +0 -8
- package/docs/images/node-gtfs-logo.svg +0 -18
- package/index.js +0 -1
- package/lib/advancedQuery.js +0 -36
- package/lib/db.js +0 -92
- package/lib/export.js +0 -141
- package/lib/file-utils.js +0 -103
- package/lib/geojson-utils.js +0 -138
- package/lib/gtfs/agencies.js +0 -32
- package/lib/gtfs/areas.js +0 -27
- package/lib/gtfs/attributions.js +0 -32
- package/lib/gtfs/booking-rules.js +0 -32
- package/lib/gtfs/calendar-dates.js +0 -32
- package/lib/gtfs/calendars.js +0 -32
- package/lib/gtfs/fare-attributes.js +0 -32
- package/lib/gtfs/fare-leg-rules.js +0 -32
- package/lib/gtfs/fare-media.js +0 -32
- package/lib/gtfs/fare-products.js +0 -32
- package/lib/gtfs/fare-rules.js +0 -32
- package/lib/gtfs/fare-transfer-rules.js +0 -32
- package/lib/gtfs/feed-info.js +0 -32
- package/lib/gtfs/frequencies.js +0 -32
- package/lib/gtfs/levels.js +0 -27
- package/lib/gtfs/location-group-stops.js +0 -32
- package/lib/gtfs/location-groups.js +0 -32
- package/lib/gtfs/locations.js +0 -32
- package/lib/gtfs/networks.js +0 -32
- package/lib/gtfs/pathways.js +0 -32
- package/lib/gtfs/route-networks.js +0 -32
- package/lib/gtfs/routes.js +0 -72
- package/lib/gtfs/shapes.js +0 -119
- package/lib/gtfs/stop-areas.js +0 -32
- package/lib/gtfs/stop-times.js +0 -32
- package/lib/gtfs/stops.js +0 -136
- package/lib/gtfs/timeframes.js +0 -32
- package/lib/gtfs/transfers.js +0 -32
- package/lib/gtfs/translations.js +0 -32
- package/lib/gtfs/trips.js +0 -27
- package/lib/gtfs-plus/calendar-attributes.js +0 -32
- package/lib/gtfs-plus/directions.js +0 -32
- package/lib/gtfs-plus/route-attributes.js +0 -32
- package/lib/gtfs-plus/stop-attributes.js +0 -32
- package/lib/gtfs-realtime/service-alerts.js +0 -34
- package/lib/gtfs-realtime/stop-time-updates.js +0 -32
- package/lib/gtfs-realtime/trip-updates.js +0 -32
- package/lib/gtfs-realtime/vehicle-positions.js +0 -32
- package/lib/gtfs-ride/board-alights.js +0 -32
- package/lib/gtfs-ride/ride-feed-infos.js +0 -32
- package/lib/gtfs-ride/rider-trips.js +0 -32
- package/lib/gtfs-ride/riderships.js +0 -32
- package/lib/gtfs-ride/trip-capacities.js +0 -32
- package/lib/gtfs.js +0 -261
- package/lib/import.js +0 -803
- package/lib/log-utils.js +0 -73
- package/lib/non-standard/timetable-notes-references.js +0 -32
- package/lib/non-standard/timetable-notes.js +0 -32
- package/lib/non-standard/timetable-pages.js +0 -32
- package/lib/non-standard/timetable-stop-order.js +0 -32
- package/lib/non-standard/timetables.js +0 -32
- package/lib/non-standard/trips-dated-vehicle-journey.js +0 -32
- package/lib/ods/deadhead-times.js +0 -32
- package/lib/ods/deadheads.js +0 -32
- package/lib/ods/ops-locations.js +0 -32
- package/lib/ods/run-events.js +0 -32
- package/lib/ods/runs-pieces.js +0 -32
- package/lib/utils.js +0 -178
- package/models/gtfs/agency.js +0 -49
- package/models/gtfs/areas.js +0 -19
- package/models/gtfs/attributions.js +0 -68
- package/models/gtfs/booking-rules.js +0 -92
- package/models/gtfs/calendar-dates.js +0 -34
- package/models/gtfs/calendar.js +0 -76
- package/models/gtfs/fare-attributes.js +0 -48
- package/models/gtfs/fare-leg-rules.js +0 -55
- package/models/gtfs/fare-media.js +0 -26
- package/models/gtfs/fare-products.js +0 -35
- package/models/gtfs/fare-rules.js +0 -34
- package/models/gtfs/fare-transfer-rules.js +0 -56
- package/models/gtfs/feed-info.js +0 -50
- package/models/gtfs/frequencies.js +0 -46
- package/models/gtfs/levels.js +0 -25
- package/models/gtfs/location-group-stops.js +0 -22
- package/models/gtfs/location-groups.js +0 -19
- package/models/gtfs/locations.js +0 -12
- package/models/gtfs/networks.js +0 -20
- package/models/gtfs/pathways.js +0 -74
- package/models/gtfs/route-networks.js +0 -21
- package/models/gtfs/routes.js +0 -79
- package/models/gtfs/shapes.js +0 -41
- package/models/gtfs/stop-areas.js +0 -20
- package/models/gtfs/stop-times.js +0 -120
- package/models/gtfs/stops.js +0 -85
- package/models/gtfs/timeframes.js +0 -29
- package/models/gtfs/transfers.js +0 -56
- package/models/gtfs/translations.js +0 -48
- package/models/gtfs/trips.js +0 -70
- package/models/gtfs-plus/calendar-attributes.js +0 -22
- package/models/gtfs-plus/directions.js +0 -29
- package/models/gtfs-plus/route-attributes.js +0 -34
- package/models/gtfs-plus/stop-attributes.js +0 -35
- package/models/gtfs-realtime/service-alert-targets.js +0 -37
- package/models/gtfs-realtime/service-alerts.js +0 -60
- package/models/gtfs-realtime/stop-time-updates.js +0 -85
- package/models/gtfs-realtime/trip-updates.js +0 -75
- package/models/gtfs-realtime/vehicle-positions.js +0 -135
- package/models/gtfs-ride/board-alight.js +0 -132
- package/models/gtfs-ride/ride-feed-info.js +0 -40
- package/models/gtfs-ride/rider-trip.js +0 -113
- package/models/gtfs-ride/ridership.js +0 -127
- package/models/gtfs-ride/trip-capacity.js +0 -51
- package/models/models.js +0 -120
- package/models/non-standard/timetable-notes-references.js +0 -50
- package/models/non-standard/timetable-notes.js +0 -24
- package/models/non-standard/timetable-pages.js +0 -23
- package/models/non-standard/timetable-stop-order.js +0 -32
- package/models/non-standard/timetables.js +0 -144
- package/models/non-standard/trips-dated-vehicle-journey.js +0 -34
- package/models/ods/deadhead-times.js +0 -65
- package/models/ods/deadheads.js +0 -60
- package/models/ods/ops-locations.js +0 -46
- package/models/ods/run-events.js +0 -70
- package/models/ods/runs-pieces.js +0 -59
- package/test/fixture/caltrain_20160406.zip +0 -0
- package/test/mocha/advanced-query.js +0 -74
- package/test/mocha/delete-db.js +0 -62
- package/test/mocha/export-gtfs.js +0 -147
- package/test/mocha/fare-transfer-rules.js +0 -32
- package/test/mocha/get-agencies.js +0 -90
- package/test/mocha/get-areas.js +0 -27
- package/test/mocha/get-attributions.js +0 -27
- package/test/mocha/get-board-alights.js +0 -28
- package/test/mocha/get-booking-rules.js +0 -28
- package/test/mocha/get-calendar-attributes.js +0 -33
- package/test/mocha/get-calendar-dates.js +0 -107
- package/test/mocha/get-calendars.js +0 -94
- package/test/mocha/get-directions.js +0 -28
- package/test/mocha/get-fare-attributes.js +0 -51
- package/test/mocha/get-fare-leg-rules.js +0 -27
- package/test/mocha/get-fare-media.js +0 -27
- package/test/mocha/get-fare-products.js +0 -27
- package/test/mocha/get-fare-rules.js +0 -50
- package/test/mocha/get-feed-info.js +0 -28
- package/test/mocha/get-frequencies.js +0 -28
- package/test/mocha/get-levels.js +0 -28
- package/test/mocha/get-location-group-stops.js +0 -33
- package/test/mocha/get-location-groups.js +0 -28
- package/test/mocha/get-locations.js +0 -69
- package/test/mocha/get-networks.js +0 -28
- package/test/mocha/get-pathways.js +0 -28
- package/test/mocha/get-ride-feed-infos.js +0 -24
- package/test/mocha/get-rider-trips.js +0 -28
- package/test/mocha/get-riderships.js +0 -28
- package/test/mocha/get-route-attributes.js +0 -33
- package/test/mocha/get-route-networks.js +0 -28
- package/test/mocha/get-routes.js +0 -143
- package/test/mocha/get-shapes-as-geojson.js +0 -92
- package/test/mocha/get-shapes.js +0 -240
- package/test/mocha/get-stop-attributes.js +0 -28
- package/test/mocha/get-stops-as-geojson.js +0 -87
- package/test/mocha/get-stops.js +0 -343
- package/test/mocha/get-stoptimes.js +0 -67
- package/test/mocha/get-timeframes.js +0 -28
- package/test/mocha/get-timetable-pages.js +0 -28
- package/test/mocha/get-timetable-stop-orders.js +0 -33
- package/test/mocha/get-timetables.js +0 -28
- package/test/mocha/get-transfers.js +0 -28
- package/test/mocha/get-translations.js +0 -28
- package/test/mocha/get-trip-capacities.js +0 -28
- package/test/mocha/get-trips.js +0 -53
- package/test/mocha/import-gtfs.js +0 -173
- package/test/mocha/open-db.js +0 -149
- package/test/mocha/raw-query.js +0 -34
- 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, rm, 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 { temporaryDirectory } from 'tempy';
|
|
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 tempPath = temporaryDirectory();
|
|
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: tempPath,
|
|
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
|
-
await rm(tempPath, { recursive: true });
|
|
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
|
-
}
|