gtfs 4.17.7 → 4.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/bin/gtfs-export.js +11 -4
- package/dist/bin/gtfs-export.js.map +1 -1
- package/dist/bin/gtfs-import.js +413 -343
- package/dist/bin/gtfs-import.js.map +1 -1
- package/dist/bin/gtfsrealtime-update.js +240 -183
- package/dist/bin/gtfsrealtime-update.js.map +1 -1
- package/dist/index.d.ts +145 -7
- package/dist/index.js +459 -377
- package/dist/index.js.map +1 -1
- package/package.json +16 -11
package/dist/bin/gtfs-import.js
CHANGED
|
@@ -4158,7 +4158,7 @@ function isValidJSON(string) {
|
|
|
4158
4158
|
try {
|
|
4159
4159
|
JSON.parse(string);
|
|
4160
4160
|
return true;
|
|
4161
|
-
} catch
|
|
4161
|
+
} catch {
|
|
4162
4162
|
return false;
|
|
4163
4163
|
}
|
|
4164
4164
|
}
|
|
@@ -4190,7 +4190,8 @@ function setDefaultConfig(initialConfig) {
|
|
|
4190
4190
|
ignoreDuplicates: false,
|
|
4191
4191
|
ignoreErrors: false,
|
|
4192
4192
|
gtfsRealtimeExpirationSeconds: 0,
|
|
4193
|
-
verbose: true
|
|
4193
|
+
verbose: true,
|
|
4194
|
+
downloadTimeout: 3e4
|
|
4194
4195
|
};
|
|
4195
4196
|
return {
|
|
4196
4197
|
...defaults,
|
|
@@ -4224,36 +4225,9 @@ function pluralize(singularWord, pluralWord, count) {
|
|
|
4224
4225
|
}
|
|
4225
4226
|
|
|
4226
4227
|
// src/lib/import-gtfs-realtime.ts
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
method: "GET",
|
|
4231
|
-
headers: {
|
|
4232
|
-
...urlConfig.headers ?? {},
|
|
4233
|
-
"Accept-Encoding": "gzip"
|
|
4234
|
-
},
|
|
4235
|
-
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
4236
|
-
});
|
|
4237
|
-
if (response.status !== 200) {
|
|
4238
|
-
task.logWarning(
|
|
4239
|
-
`Unable to download GTFS-Realtime from ${urlConfig.url}. Got status ${response.status}.`
|
|
4240
|
-
);
|
|
4241
|
-
return null;
|
|
4242
|
-
}
|
|
4243
|
-
const buffer = await response.arrayBuffer();
|
|
4244
|
-
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
4245
|
-
new Uint8Array(buffer)
|
|
4246
|
-
);
|
|
4247
|
-
return GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
4248
|
-
enums: String,
|
|
4249
|
-
longs: String,
|
|
4250
|
-
bytes: String,
|
|
4251
|
-
defaults: false,
|
|
4252
|
-
arrays: true,
|
|
4253
|
-
objects: true,
|
|
4254
|
-
oneofs: true
|
|
4255
|
-
});
|
|
4256
|
-
}
|
|
4228
|
+
var BATCH_SIZE = 1e3;
|
|
4229
|
+
var MAX_RETRIES = 3;
|
|
4230
|
+
var RETRY_DELAY = 1e3;
|
|
4257
4231
|
function prepareRealtimeFieldValue(entity, column, task) {
|
|
4258
4232
|
if (column.name === "created_timestamp") {
|
|
4259
4233
|
return task.currentTimestamp;
|
|
@@ -4270,163 +4244,246 @@ function prepareRealtimeFieldValue(entity, column, task) {
|
|
|
4270
4244
|
);
|
|
4271
4245
|
return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
|
|
4272
4246
|
}
|
|
4273
|
-
|
|
4274
|
-
const
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
);
|
|
4279
|
-
const informedEntityStmt = db.prepare(
|
|
4280
|
-
`REPLACE INTO ${serviceAlertInformedEntities.filenameBase} (${serviceAlertInformedEntities.schema.map((column) => column.name).join(
|
|
4281
|
-
", "
|
|
4282
|
-
)}) VALUES (${serviceAlertInformedEntities.schema.map(() => "?").join(", ")})`
|
|
4247
|
+
function createPreparedStatement(db, model) {
|
|
4248
|
+
const columns = model.schema.map((column) => column.name);
|
|
4249
|
+
const placeholders = model.schema.map(() => "?").join(", ");
|
|
4250
|
+
return db.prepare(
|
|
4251
|
+
`REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
|
|
4283
4252
|
);
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4253
|
+
}
|
|
4254
|
+
async function processBatch(items, batchSize, processor) {
|
|
4255
|
+
let totalRecordCount = 0;
|
|
4256
|
+
let totalErrorCount = 0;
|
|
4257
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
4258
|
+
const batch = items.slice(i, i + batchSize);
|
|
4259
|
+
try {
|
|
4260
|
+
const result = await processor(batch);
|
|
4261
|
+
totalRecordCount += result.recordCount;
|
|
4262
|
+
totalErrorCount += result.errorCount;
|
|
4263
|
+
} catch (error) {
|
|
4264
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4265
|
+
totalErrorCount += batch.length;
|
|
4266
|
+
console.error(`Batch processing error: ${errorMessage}`);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
return { recordCount: totalRecordCount, errorCount: totalErrorCount };
|
|
4270
|
+
}
|
|
4271
|
+
async function fetchGtfsRealtimeData(type, task) {
|
|
4272
|
+
const urlConfig = getUrlConfig(type, task);
|
|
4273
|
+
if (!urlConfig) {
|
|
4274
|
+
return null;
|
|
4275
|
+
}
|
|
4276
|
+
task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
|
|
4277
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
4278
|
+
try {
|
|
4279
|
+
const response = await fetch(urlConfig.url, {
|
|
4280
|
+
method: "GET",
|
|
4281
|
+
headers: {
|
|
4282
|
+
...urlConfig.headers ?? {},
|
|
4283
|
+
"Accept-Encoding": "gzip"
|
|
4284
|
+
},
|
|
4285
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
4286
|
+
});
|
|
4287
|
+
if (response.status !== 200) {
|
|
4288
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4289
|
+
}
|
|
4290
|
+
const buffer = await response.arrayBuffer();
|
|
4291
|
+
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
4292
|
+
new Uint8Array(buffer)
|
|
4289
4293
|
);
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4294
|
+
const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
4295
|
+
enums: String,
|
|
4296
|
+
longs: String,
|
|
4297
|
+
bytes: String,
|
|
4298
|
+
defaults: false,
|
|
4299
|
+
arrays: true,
|
|
4300
|
+
objects: true,
|
|
4301
|
+
oneofs: true
|
|
4302
|
+
});
|
|
4303
|
+
return feedMessage;
|
|
4304
|
+
} catch (error) {
|
|
4305
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4306
|
+
if (attempt === MAX_RETRIES) {
|
|
4307
|
+
if (task.ignoreErrors) {
|
|
4308
|
+
task.logError(
|
|
4309
|
+
`Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
|
|
4310
|
+
);
|
|
4311
|
+
return null;
|
|
4312
|
+
}
|
|
4313
|
+
throw error;
|
|
4314
|
+
}
|
|
4315
|
+
task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
|
|
4316
|
+
await new Promise(
|
|
4317
|
+
(resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
|
|
4318
|
+
);
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
return null;
|
|
4322
|
+
}
|
|
4323
|
+
function getUrlConfig(type, task) {
|
|
4324
|
+
switch (type) {
|
|
4325
|
+
case "alerts":
|
|
4326
|
+
return task.realtimeAlerts;
|
|
4327
|
+
case "tripupdates":
|
|
4328
|
+
return task.realtimeTripUpdates;
|
|
4329
|
+
case "vehiclepositions":
|
|
4330
|
+
return task.realtimeVehiclePositions;
|
|
4331
|
+
default:
|
|
4332
|
+
return void 0;
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
function createServiceAlertsProcessor(db, task) {
|
|
4336
|
+
const alertStmt = createPreparedStatement(db, serviceAlerts);
|
|
4337
|
+
const informedEntityStmt = createPreparedStatement(
|
|
4338
|
+
db,
|
|
4339
|
+
serviceAlertInformedEntities
|
|
4340
|
+
);
|
|
4341
|
+
return async (batch) => {
|
|
4342
|
+
let recordCount = 0;
|
|
4343
|
+
let errorCount = 0;
|
|
4344
|
+
db.transaction(() => {
|
|
4345
|
+
for (const entity of batch) {
|
|
4346
|
+
try {
|
|
4347
|
+
const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4348
|
+
alertStmt.run(alertValues);
|
|
4349
|
+
recordCount++;
|
|
4350
|
+
if (entity.alert?.informedEntity?.length) {
|
|
4351
|
+
for (const informedEntity of entity.alert.informedEntity) {
|
|
4295
4352
|
informedEntity.parent = entity;
|
|
4296
|
-
|
|
4353
|
+
const entityValues = serviceAlertInformedEntities.schema.map(
|
|
4297
4354
|
(column) => prepareRealtimeFieldValue(informedEntity, column, task)
|
|
4298
4355
|
);
|
|
4356
|
+
informedEntityStmt.run(entityValues);
|
|
4357
|
+
recordCount++;
|
|
4299
4358
|
}
|
|
4300
|
-
);
|
|
4301
|
-
for (const values of informedEntities) {
|
|
4302
|
-
informedEntityStmt.run(values);
|
|
4303
4359
|
}
|
|
4360
|
+
} catch (error) {
|
|
4361
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4362
|
+
errorCount++;
|
|
4363
|
+
task.logWarning(`Alert processing error: ${errorMessage}`);
|
|
4304
4364
|
}
|
|
4305
|
-
totalLineCount++;
|
|
4306
|
-
} catch (error) {
|
|
4307
|
-
task.logWarning(`Import error: ${error.message}`);
|
|
4308
4365
|
}
|
|
4309
|
-
}
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
true
|
|
4313
|
-
);
|
|
4314
|
-
})();
|
|
4366
|
+
})();
|
|
4367
|
+
return { recordCount, errorCount };
|
|
4368
|
+
};
|
|
4315
4369
|
}
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
", "
|
|
4321
|
-
)}) VALUES (${tripUpdates.schema.map(() => "?").join(", ")})`
|
|
4370
|
+
function createTripUpdatesProcessor(db, task) {
|
|
4371
|
+
const tripUpdateStmt = createPreparedStatement(
|
|
4372
|
+
db,
|
|
4373
|
+
tripUpdates
|
|
4322
4374
|
);
|
|
4323
|
-
const stopTimeStmt =
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
)}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
|
|
4375
|
+
const stopTimeStmt = createPreparedStatement(
|
|
4376
|
+
db,
|
|
4377
|
+
stopTimeUpdates
|
|
4327
4378
|
);
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
(
|
|
4339
|
-
|
|
4340
|
-
|
|
4379
|
+
return async (batch) => {
|
|
4380
|
+
let recordCount = 0;
|
|
4381
|
+
let errorCount = 0;
|
|
4382
|
+
db.transaction(() => {
|
|
4383
|
+
for (const entity of batch) {
|
|
4384
|
+
try {
|
|
4385
|
+
const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4386
|
+
tripUpdateStmt.run(tripUpdateValues);
|
|
4387
|
+
recordCount++;
|
|
4388
|
+
if (entity.tripUpdate?.stopTimeUpdate?.length) {
|
|
4389
|
+
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
|
|
4390
|
+
stopTimeUpdate.parent = entity;
|
|
4391
|
+
const stopTimeValues = stopTimeUpdates.schema.map(
|
|
4392
|
+
(column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
|
|
4393
|
+
);
|
|
4394
|
+
stopTimeStmt.run(stopTimeValues);
|
|
4395
|
+
recordCount++;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
} catch (error) {
|
|
4399
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4400
|
+
errorCount++;
|
|
4401
|
+
task.logWarning(`Trip update processing error: ${errorMessage}`);
|
|
4341
4402
|
}
|
|
4342
|
-
totalLineCount++;
|
|
4343
|
-
} catch (error) {
|
|
4344
|
-
task.logWarning(`Import error: ${error.message}`);
|
|
4345
4403
|
}
|
|
4346
|
-
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
true
|
|
4350
|
-
);
|
|
4351
|
-
})();
|
|
4404
|
+
})();
|
|
4405
|
+
return { recordCount, errorCount };
|
|
4406
|
+
};
|
|
4352
4407
|
}
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
", "
|
|
4358
|
-
)}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
|
|
4408
|
+
function createVehiclePositionsProcessor(db, task) {
|
|
4409
|
+
const vehiclePositionStmt = createPreparedStatement(
|
|
4410
|
+
db,
|
|
4411
|
+
vehiclePositions
|
|
4359
4412
|
);
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4413
|
+
return async (batch) => {
|
|
4414
|
+
let recordCount = 0;
|
|
4415
|
+
let errorCount = 0;
|
|
4416
|
+
db.transaction(() => {
|
|
4417
|
+
for (const entity of batch) {
|
|
4418
|
+
try {
|
|
4419
|
+
const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4420
|
+
vehiclePositionStmt.run(fieldValues);
|
|
4421
|
+
recordCount++;
|
|
4422
|
+
} catch (error) {
|
|
4423
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4424
|
+
errorCount++;
|
|
4425
|
+
task.logWarning(`Vehicle position processing error: ${errorMessage}`);
|
|
4426
|
+
}
|
|
4368
4427
|
}
|
|
4369
|
-
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
true
|
|
4373
|
-
);
|
|
4374
|
-
})();
|
|
4428
|
+
})();
|
|
4429
|
+
return { recordCount, errorCount };
|
|
4430
|
+
};
|
|
4375
4431
|
}
|
|
4376
4432
|
async function updateGtfsRealtimeData(task) {
|
|
4377
|
-
if (task.realtimeAlerts
|
|
4433
|
+
if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
|
|
4378
4434
|
return;
|
|
4379
4435
|
}
|
|
4436
|
+
const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
|
|
4437
|
+
[
|
|
4438
|
+
task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
|
|
4439
|
+
task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
|
|
4440
|
+
task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
|
|
4441
|
+
]
|
|
4442
|
+
);
|
|
4380
4443
|
const db = openDb({ sqlitePath: task.sqlitePath });
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4444
|
+
const recordCounts = {
|
|
4445
|
+
alerts: 0,
|
|
4446
|
+
tripupdates: 0,
|
|
4447
|
+
vehiclepositions: 0
|
|
4448
|
+
};
|
|
4449
|
+
const processingPromises = [];
|
|
4450
|
+
if (alertsData?.entity?.length) {
|
|
4451
|
+
processingPromises.push(
|
|
4452
|
+
processBatch(
|
|
4453
|
+
alertsData.entity,
|
|
4454
|
+
BATCH_SIZE,
|
|
4455
|
+
createServiceAlertsProcessor(db, task)
|
|
4456
|
+
).then((result) => {
|
|
4457
|
+
recordCounts.alerts = result.recordCount;
|
|
4458
|
+
})
|
|
4459
|
+
);
|
|
4394
4460
|
}
|
|
4395
|
-
if (
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
if (task.ignoreErrors) {
|
|
4406
|
-
task.logError(error.message);
|
|
4407
|
-
} else {
|
|
4408
|
-
throw error;
|
|
4409
|
-
}
|
|
4410
|
-
}
|
|
4461
|
+
if (tripUpdatesData?.entity?.length) {
|
|
4462
|
+
processingPromises.push(
|
|
4463
|
+
processBatch(
|
|
4464
|
+
tripUpdatesData.entity,
|
|
4465
|
+
BATCH_SIZE,
|
|
4466
|
+
createTripUpdatesProcessor(db, task)
|
|
4467
|
+
).then((result) => {
|
|
4468
|
+
recordCounts.tripupdates = result.recordCount;
|
|
4469
|
+
})
|
|
4470
|
+
);
|
|
4411
4471
|
}
|
|
4412
|
-
if (
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
}
|
|
4421
|
-
|
|
4422
|
-
if (task.ignoreErrors) {
|
|
4423
|
-
task.logError(error.message);
|
|
4424
|
-
} else {
|
|
4425
|
-
throw error;
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
4472
|
+
if (vehiclePositionsData?.entity?.length) {
|
|
4473
|
+
processingPromises.push(
|
|
4474
|
+
processBatch(
|
|
4475
|
+
vehiclePositionsData.entity,
|
|
4476
|
+
BATCH_SIZE,
|
|
4477
|
+
createVehiclePositionsProcessor(db, task)
|
|
4478
|
+
).then((result) => {
|
|
4479
|
+
recordCounts.vehiclepositions = result.recordCount;
|
|
4480
|
+
})
|
|
4481
|
+
);
|
|
4428
4482
|
}
|
|
4429
|
-
|
|
4483
|
+
await Promise.all(processingPromises);
|
|
4484
|
+
task.log(
|
|
4485
|
+
`GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
|
|
4486
|
+
);
|
|
4430
4487
|
}
|
|
4431
4488
|
|
|
4432
4489
|
// src/lib/import-gtfs.ts
|
|
@@ -4459,7 +4516,7 @@ var extractGtfsFiles = async (task) => {
|
|
|
4459
4516
|
throw new Error("No `path` specified in config");
|
|
4460
4517
|
}
|
|
4461
4518
|
const gtfsPath = untildify(task.path);
|
|
4462
|
-
task.log(`Importing GTFS from ${task.path}\r`);
|
|
4519
|
+
task.log(`Importing static GTFS from ${task.path}\r`);
|
|
4463
4520
|
if (path2.extname(gtfsPath) === ".zip") {
|
|
4464
4521
|
try {
|
|
4465
4522
|
await unzip(gtfsPath, task.downloadDir);
|
|
@@ -4542,11 +4599,11 @@ var createGtfsTables = (db) => {
|
|
|
4542
4599
|
if (column.type === "time") {
|
|
4543
4600
|
sqlColumnCreateStatements.push(
|
|
4544
4601
|
`${getTimestampColumnName(column.name)} INTEGER GENERATED ALWAYS AS (
|
|
4545
|
-
CASE
|
|
4546
|
-
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
4602
|
+
CASE
|
|
4603
|
+
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
4547
4604
|
ELSE CAST(
|
|
4548
|
-
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
4549
|
-
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
4605
|
+
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
4606
|
+
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
4550
4607
|
substr(${column.name}, -2) AS INTEGER
|
|
4551
4608
|
)
|
|
4552
4609
|
END
|
|
@@ -4603,7 +4660,7 @@ var formatGtfsLine = (line, model, totalLineCount) => {
|
|
|
4603
4660
|
continue;
|
|
4604
4661
|
}
|
|
4605
4662
|
if (type === "date") {
|
|
4606
|
-
value = value.replace(/-/g, "");
|
|
4663
|
+
value = value?.toString().replace(/-/g, "");
|
|
4607
4664
|
if (value.length !== 8) {
|
|
4608
4665
|
throw new Error(
|
|
4609
4666
|
`Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
|
|
@@ -4619,203 +4676,212 @@ var formatGtfsLine = (line, model, totalLineCount) => {
|
|
|
4619
4676
|
}
|
|
4620
4677
|
return formattedLine;
|
|
4621
4678
|
};
|
|
4622
|
-
var
|
|
4623
|
-
var importGtfsFiles = (db, task) =>
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
task.
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
if (model.extension === "gtfs-realtime") {
|
|
4634
|
-
resolve();
|
|
4635
|
-
return;
|
|
4636
|
-
}
|
|
4637
|
-
const filepath = path2.join(task.downloadDir, `${filename}`);
|
|
4638
|
-
if (!existsSync2(filepath)) {
|
|
4639
|
-
if (!model.nonstandard) {
|
|
4640
|
-
task.log(`Importing - ${filename} - No file found\r`);
|
|
4679
|
+
var BATCH_SIZE2 = 1e5;
|
|
4680
|
+
var importGtfsFiles = async (db, task) => {
|
|
4681
|
+
await mapSeries2(
|
|
4682
|
+
Object.values(models_exports),
|
|
4683
|
+
(model) => new Promise((resolve, reject) => {
|
|
4684
|
+
let totalLineCount = 0;
|
|
4685
|
+
const filename = `${model.filenameBase}.${model.filenameExtension}`;
|
|
4686
|
+
if (task.exclude && task.exclude.includes(model.filenameBase)) {
|
|
4687
|
+
task.log(`Skipping - ${filename}\r`);
|
|
4688
|
+
resolve();
|
|
4689
|
+
return;
|
|
4641
4690
|
}
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
|
|
4651
|
-
const insert = db.prepare(prepareStatement);
|
|
4652
|
-
const insertLines = db.transaction((lines) => {
|
|
4653
|
-
for (const [rowNumber, line] of Object.entries(lines)) {
|
|
4654
|
-
try {
|
|
4655
|
-
if (task.prefix === void 0) {
|
|
4656
|
-
insert.run(line);
|
|
4657
|
-
} else {
|
|
4658
|
-
const prefixedLine = Object.fromEntries(
|
|
4659
|
-
Object.entries(
|
|
4660
|
-
line
|
|
4661
|
-
).map(([columnName, value]) => [
|
|
4662
|
-
columnName,
|
|
4663
|
-
applyPrefixToValue(
|
|
4664
|
-
value,
|
|
4665
|
-
prefixedColumns.has(columnName),
|
|
4666
|
-
task.prefix
|
|
4667
|
-
)
|
|
4668
|
-
])
|
|
4669
|
-
);
|
|
4670
|
-
insert.run(prefixedLine);
|
|
4671
|
-
}
|
|
4672
|
-
} catch (error) {
|
|
4673
|
-
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
4674
|
-
const primaryColumns = columns.filter(
|
|
4675
|
-
(column) => column.primary
|
|
4676
|
-
);
|
|
4677
|
-
task.logWarning(
|
|
4678
|
-
`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`
|
|
4679
|
-
);
|
|
4680
|
-
}
|
|
4681
|
-
task.logWarning(
|
|
4682
|
-
`Check ${filename} for invalid data on line ${rowNumber + 1}.`
|
|
4683
|
-
);
|
|
4684
|
-
throw error;
|
|
4691
|
+
if (model.extension === "gtfs-realtime") {
|
|
4692
|
+
resolve();
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
const filepath = path2.join(task.downloadDir, `${filename}`);
|
|
4696
|
+
if (!existsSync2(filepath)) {
|
|
4697
|
+
if (!model.nonstandard) {
|
|
4698
|
+
task.log(`Importing - ${filename} - No file found\r`);
|
|
4685
4699
|
}
|
|
4700
|
+
resolve();
|
|
4701
|
+
return;
|
|
4686
4702
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
const
|
|
4690
|
-
columns
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4703
|
+
task.log(`Importing - ${filename}\r`);
|
|
4704
|
+
const columns = model.schema;
|
|
4705
|
+
const prefixedColumns = new Set(
|
|
4706
|
+
columns.filter((column) => column.prefix).map((column) => column.name)
|
|
4707
|
+
);
|
|
4708
|
+
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
|
|
4709
|
+
const insert = db.prepare(prepareStatement);
|
|
4710
|
+
const insertLines = db.transaction((lines) => {
|
|
4711
|
+
for (const [rowNumber, line] of Object.entries(lines)) {
|
|
4712
|
+
try {
|
|
4713
|
+
if (task.prefix === void 0) {
|
|
4714
|
+
insert.run(line);
|
|
4715
|
+
} else {
|
|
4716
|
+
const prefixedLine = Object.fromEntries(
|
|
4717
|
+
Object.entries(
|
|
4718
|
+
line
|
|
4719
|
+
).map(([columnName, value]) => [
|
|
4720
|
+
columnName,
|
|
4721
|
+
applyPrefixToValue(
|
|
4722
|
+
value,
|
|
4723
|
+
prefixedColumns.has(columnName),
|
|
4724
|
+
task.prefix
|
|
4725
|
+
)
|
|
4726
|
+
])
|
|
4709
4727
|
);
|
|
4728
|
+
insert.run(prefixedLine);
|
|
4710
4729
|
}
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4730
|
+
} catch (error) {
|
|
4731
|
+
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
4732
|
+
const primaryColumns = columns.filter(
|
|
4733
|
+
(column) => column.primary
|
|
4734
|
+
);
|
|
4735
|
+
task.logWarning(
|
|
4736
|
+
`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`
|
|
4737
|
+
);
|
|
4738
|
+
}
|
|
4739
|
+
task.logWarning(
|
|
4740
|
+
`Check ${filename} for invalid data on line ${rowNumber + 1}.`
|
|
4741
|
+
);
|
|
4742
|
+
throw error;
|
|
4718
4743
|
}
|
|
4719
4744
|
}
|
|
4720
4745
|
});
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4746
|
+
if (model.filenameExtension === "txt") {
|
|
4747
|
+
const parser = parse({
|
|
4748
|
+
columns: true,
|
|
4749
|
+
relax_quotes: true,
|
|
4750
|
+
trim: true,
|
|
4751
|
+
skip_empty_lines: true,
|
|
4752
|
+
...task.csvOptions
|
|
4753
|
+
});
|
|
4754
|
+
let lines = [];
|
|
4755
|
+
parser.on("readable", () => {
|
|
4756
|
+
try {
|
|
4757
|
+
let record;
|
|
4758
|
+
while (record = parser.read()) {
|
|
4759
|
+
totalLineCount += 1;
|
|
4760
|
+
lines.push(formatGtfsLine(record, model, totalLineCount));
|
|
4761
|
+
if (lines.length >= BATCH_SIZE2) {
|
|
4762
|
+
insertLines(lines);
|
|
4763
|
+
lines = [];
|
|
4764
|
+
task.log(
|
|
4765
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4766
|
+
true
|
|
4730
4767
|
);
|
|
4731
|
-
resolve();
|
|
4732
|
-
return;
|
|
4733
|
-
} else {
|
|
4734
|
-
reject(error);
|
|
4735
|
-
return;
|
|
4736
4768
|
}
|
|
4737
4769
|
}
|
|
4770
|
+
} catch (error) {
|
|
4771
|
+
if (task.ignoreErrors) {
|
|
4772
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4773
|
+
task.logError(`Error processing ${filename}: ${errorMessage}`);
|
|
4774
|
+
resolve();
|
|
4775
|
+
} else {
|
|
4776
|
+
reject(error);
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
});
|
|
4780
|
+
parser.on("end", () => {
|
|
4781
|
+
try {
|
|
4782
|
+
if (lines.length > 0) {
|
|
4783
|
+
try {
|
|
4784
|
+
insertLines(lines);
|
|
4785
|
+
} catch (error) {
|
|
4786
|
+
if (task.ignoreErrors) {
|
|
4787
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4788
|
+
task.logError(
|
|
4789
|
+
`Error inserting data for ${filename}: ${errorMessage}`
|
|
4790
|
+
);
|
|
4791
|
+
resolve();
|
|
4792
|
+
return;
|
|
4793
|
+
} else {
|
|
4794
|
+
reject(error);
|
|
4795
|
+
return;
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
task.log(
|
|
4800
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4801
|
+
true
|
|
4802
|
+
);
|
|
4803
|
+
resolve();
|
|
4804
|
+
} catch (error) {
|
|
4805
|
+
if (task.ignoreErrors) {
|
|
4806
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4807
|
+
task.logError(`Error finalizing ${filename}: ${errorMessage}`);
|
|
4808
|
+
resolve();
|
|
4809
|
+
} else {
|
|
4810
|
+
reject(error);
|
|
4811
|
+
}
|
|
4738
4812
|
}
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
true
|
|
4742
|
-
);
|
|
4743
|
-
resolve();
|
|
4744
|
-
} catch (error) {
|
|
4813
|
+
});
|
|
4814
|
+
parser.on("error", (error) => {
|
|
4745
4815
|
if (task.ignoreErrors) {
|
|
4746
|
-
|
|
4816
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4817
|
+
task.logError(`Parser error for ${filename}: ${errorMessage}`);
|
|
4747
4818
|
resolve();
|
|
4748
4819
|
} else {
|
|
4749
4820
|
reject(error);
|
|
4750
4821
|
}
|
|
4751
|
-
}
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
if (isValidJSON(data) === false) {
|
|
4765
|
-
if (task.ignoreErrors) {
|
|
4766
|
-
task.logError(`Invalid JSON in ${filename}`);
|
|
4767
|
-
resolve();
|
|
4768
|
-
return;
|
|
4769
|
-
} else {
|
|
4770
|
-
reject(new Error(`Invalid JSON in ${filename}`));
|
|
4771
|
-
return;
|
|
4822
|
+
});
|
|
4823
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
4824
|
+
} else if (model.filenameExtension === "geojson") {
|
|
4825
|
+
readFile2(filepath, "utf8").then((data) => {
|
|
4826
|
+
if (isValidJSON(data) === false) {
|
|
4827
|
+
if (task.ignoreErrors) {
|
|
4828
|
+
task.logError(`Invalid JSON in ${filename}`);
|
|
4829
|
+
resolve();
|
|
4830
|
+
return;
|
|
4831
|
+
} else {
|
|
4832
|
+
reject(new Error(`Invalid JSON in ${filename}`));
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4772
4835
|
}
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
totalLineCount
|
|
4779
|
-
);
|
|
4780
|
-
try {
|
|
4781
|
-
insertLines([line]);
|
|
4782
|
-
task.log(
|
|
4783
|
-
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4784
|
-
true
|
|
4836
|
+
totalLineCount += 1;
|
|
4837
|
+
const line = formatGtfsLine(
|
|
4838
|
+
{ geojson: data },
|
|
4839
|
+
model,
|
|
4840
|
+
totalLineCount
|
|
4785
4841
|
);
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4842
|
+
try {
|
|
4843
|
+
insertLines([line]);
|
|
4844
|
+
task.log(
|
|
4845
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4846
|
+
true
|
|
4791
4847
|
);
|
|
4792
4848
|
resolve();
|
|
4849
|
+
} catch (error) {
|
|
4850
|
+
if (task.ignoreErrors) {
|
|
4851
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4852
|
+
task.logError(
|
|
4853
|
+
`Error inserting data for ${filename}: ${errorMessage}`
|
|
4854
|
+
);
|
|
4855
|
+
resolve();
|
|
4856
|
+
} else {
|
|
4857
|
+
reject(error);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
}).catch((error) => {
|
|
4861
|
+
if (task.ignoreErrors) {
|
|
4862
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4863
|
+
task.logError(`Error reading ${filename}: ${errorMessage}`);
|
|
4864
|
+
resolve();
|
|
4793
4865
|
} else {
|
|
4794
4866
|
reject(error);
|
|
4795
4867
|
}
|
|
4796
|
-
}
|
|
4797
|
-
}
|
|
4868
|
+
});
|
|
4869
|
+
} else {
|
|
4798
4870
|
if (task.ignoreErrors) {
|
|
4799
|
-
task.logError(
|
|
4871
|
+
task.logError(
|
|
4872
|
+
`Unsupported file type: ${model.filenameExtension} for ${filename}`
|
|
4873
|
+
);
|
|
4800
4874
|
resolve();
|
|
4801
4875
|
} else {
|
|
4802
|
-
reject(
|
|
4876
|
+
reject(
|
|
4877
|
+
new Error(`Unsupported file type: ${model.filenameExtension}`)
|
|
4878
|
+
);
|
|
4803
4879
|
}
|
|
4804
|
-
});
|
|
4805
|
-
} else {
|
|
4806
|
-
if (task.ignoreErrors) {
|
|
4807
|
-
task.logError(
|
|
4808
|
-
`Unsupported file type: ${model.filenameExtension} for ${filename}`
|
|
4809
|
-
);
|
|
4810
|
-
resolve();
|
|
4811
|
-
} else {
|
|
4812
|
-
reject(
|
|
4813
|
-
new Error(`Unsupported file type: ${model.filenameExtension}`)
|
|
4814
|
-
);
|
|
4815
4880
|
}
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
);
|
|
4881
|
+
})
|
|
4882
|
+
);
|
|
4883
|
+
task.log(`Static GTFS import complete`);
|
|
4884
|
+
};
|
|
4819
4885
|
async function importGtfs(initialConfig) {
|
|
4820
4886
|
const startTime = process.hrtime.bigint();
|
|
4821
4887
|
const config = setDefaultConfig(initialConfig);
|
|
@@ -4832,7 +4898,6 @@ async function importGtfs(initialConfig) {
|
|
|
4832
4898
|
const tempPath = temporaryDirectory();
|
|
4833
4899
|
const task = {
|
|
4834
4900
|
exclude: agency2.exclude,
|
|
4835
|
-
url: agency2.url,
|
|
4836
4901
|
headers: agency2.headers,
|
|
4837
4902
|
realtimeAlerts: agency2.realtimeAlerts,
|
|
4838
4903
|
realtimeTripUpdates: agency2.realtimeTripUpdates,
|
|
@@ -4840,7 +4905,6 @@ async function importGtfs(initialConfig) {
|
|
|
4840
4905
|
downloadDir: tempPath,
|
|
4841
4906
|
downloadTimeout: config.downloadTimeout,
|
|
4842
4907
|
gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
|
|
4843
|
-
path: agency2.path,
|
|
4844
4908
|
csvOptions: config.csvOptions || {},
|
|
4845
4909
|
ignoreDuplicates: config.ignoreDuplicates,
|
|
4846
4910
|
ignoreErrors: config.ignoreErrors,
|
|
@@ -4851,8 +4915,13 @@ async function importGtfs(initialConfig) {
|
|
|
4851
4915
|
logWarning: logWarning(config),
|
|
4852
4916
|
logError: logError(config)
|
|
4853
4917
|
};
|
|
4854
|
-
if (
|
|
4918
|
+
if ("url" in agency2) {
|
|
4919
|
+
Object.assign(task, { url: agency2.url });
|
|
4855
4920
|
await downloadGtfsFiles(task);
|
|
4921
|
+
} else {
|
|
4922
|
+
Object.assign(task, {
|
|
4923
|
+
path: agency2.path
|
|
4924
|
+
});
|
|
4856
4925
|
}
|
|
4857
4926
|
await extractGtfsFiles(task);
|
|
4858
4927
|
await importGtfsFiles(db, task);
|
|
@@ -4860,7 +4929,8 @@ async function importGtfs(initialConfig) {
|
|
|
4860
4929
|
await rm2(tempPath, { recursive: true });
|
|
4861
4930
|
} catch (error) {
|
|
4862
4931
|
if (config.ignoreErrors) {
|
|
4863
|
-
|
|
4932
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4933
|
+
logError(config)(errorMessage);
|
|
4864
4934
|
} else {
|
|
4865
4935
|
throw error;
|
|
4866
4936
|
}
|
|
@@ -4875,7 +4945,7 @@ async function importGtfs(initialConfig) {
|
|
|
4875
4945
|
`
|
|
4876
4946
|
);
|
|
4877
4947
|
} catch (error) {
|
|
4878
|
-
if (error
|
|
4948
|
+
if (error.code === "SQLITE_CANTOPEN") {
|
|
4879
4949
|
logError(config)(
|
|
4880
4950
|
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
4881
4951
|
);
|