gtfs 4.17.7 → 4.18.1
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 +14 -14
- package/dist/bin/gtfs-export.js +29 -4
- package/dist/bin/gtfs-export.js.map +1 -1
- package/dist/bin/gtfs-import.js +431 -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 +431 -290
- package/dist/index.js +478 -383
- package/dist/index.js.map +1 -1
- package/dist/models/models.d.ts +19 -0
- package/dist/models/models.js +18 -0
- package/dist/models/models.js.map +1 -1
- package/package.json +18 -13
package/dist/bin/gtfs-import.js
CHANGED
|
@@ -248,6 +248,12 @@ var agency = {
|
|
|
248
248
|
name: "agency_email",
|
|
249
249
|
type: "text",
|
|
250
250
|
nocase: true
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "cemv_support",
|
|
254
|
+
type: "integer",
|
|
255
|
+
min: 0,
|
|
256
|
+
max: 2
|
|
251
257
|
}
|
|
252
258
|
]
|
|
253
259
|
};
|
|
@@ -1178,6 +1184,12 @@ var routes = {
|
|
|
1178
1184
|
name: "network_id",
|
|
1179
1185
|
type: "text",
|
|
1180
1186
|
prefix: true
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
name: "cemv_support",
|
|
1190
|
+
type: "integer",
|
|
1191
|
+
min: 0,
|
|
1192
|
+
max: 2
|
|
1181
1193
|
}
|
|
1182
1194
|
]
|
|
1183
1195
|
};
|
|
@@ -1432,6 +1444,12 @@ var stops = {
|
|
|
1432
1444
|
{
|
|
1433
1445
|
name: "platform_code",
|
|
1434
1446
|
type: "text"
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
name: "stop_access",
|
|
1450
|
+
type: "integer",
|
|
1451
|
+
min: 0,
|
|
1452
|
+
max: 1
|
|
1435
1453
|
}
|
|
1436
1454
|
]
|
|
1437
1455
|
};
|
|
@@ -4158,7 +4176,7 @@ function isValidJSON(string) {
|
|
|
4158
4176
|
try {
|
|
4159
4177
|
JSON.parse(string);
|
|
4160
4178
|
return true;
|
|
4161
|
-
} catch
|
|
4179
|
+
} catch {
|
|
4162
4180
|
return false;
|
|
4163
4181
|
}
|
|
4164
4182
|
}
|
|
@@ -4190,7 +4208,8 @@ function setDefaultConfig(initialConfig) {
|
|
|
4190
4208
|
ignoreDuplicates: false,
|
|
4191
4209
|
ignoreErrors: false,
|
|
4192
4210
|
gtfsRealtimeExpirationSeconds: 0,
|
|
4193
|
-
verbose: true
|
|
4211
|
+
verbose: true,
|
|
4212
|
+
downloadTimeout: 3e4
|
|
4194
4213
|
};
|
|
4195
4214
|
return {
|
|
4196
4215
|
...defaults,
|
|
@@ -4224,36 +4243,9 @@ function pluralize(singularWord, pluralWord, count) {
|
|
|
4224
4243
|
}
|
|
4225
4244
|
|
|
4226
4245
|
// 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
|
-
}
|
|
4246
|
+
var BATCH_SIZE = 1e3;
|
|
4247
|
+
var MAX_RETRIES = 3;
|
|
4248
|
+
var RETRY_DELAY = 1e3;
|
|
4257
4249
|
function prepareRealtimeFieldValue(entity, column, task) {
|
|
4258
4250
|
if (column.name === "created_timestamp") {
|
|
4259
4251
|
return task.currentTimestamp;
|
|
@@ -4270,163 +4262,246 @@ function prepareRealtimeFieldValue(entity, column, task) {
|
|
|
4270
4262
|
);
|
|
4271
4263
|
return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
|
|
4272
4264
|
}
|
|
4273
|
-
|
|
4274
|
-
const
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4265
|
+
function createPreparedStatement(db, model) {
|
|
4266
|
+
const columns = model.schema.map((column) => column.name);
|
|
4267
|
+
const placeholders = model.schema.map(() => "?").join(", ");
|
|
4268
|
+
return db.prepare(
|
|
4269
|
+
`REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
|
|
4278
4270
|
);
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
)
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4271
|
+
}
|
|
4272
|
+
async function processBatch(items, batchSize, processor) {
|
|
4273
|
+
let totalRecordCount = 0;
|
|
4274
|
+
let totalErrorCount = 0;
|
|
4275
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
4276
|
+
const batch = items.slice(i, i + batchSize);
|
|
4277
|
+
try {
|
|
4278
|
+
const result = await processor(batch);
|
|
4279
|
+
totalRecordCount += result.recordCount;
|
|
4280
|
+
totalErrorCount += result.errorCount;
|
|
4281
|
+
} catch (error) {
|
|
4282
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4283
|
+
totalErrorCount += batch.length;
|
|
4284
|
+
console.error(`Batch processing error: ${errorMessage}`);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
return { recordCount: totalRecordCount, errorCount: totalErrorCount };
|
|
4288
|
+
}
|
|
4289
|
+
async function fetchGtfsRealtimeData(type, task) {
|
|
4290
|
+
const urlConfig = getUrlConfig(type, task);
|
|
4291
|
+
if (!urlConfig) {
|
|
4292
|
+
return null;
|
|
4293
|
+
}
|
|
4294
|
+
task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
|
|
4295
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
4296
|
+
try {
|
|
4297
|
+
const response = await fetch(urlConfig.url, {
|
|
4298
|
+
method: "GET",
|
|
4299
|
+
headers: {
|
|
4300
|
+
...urlConfig.headers ?? {},
|
|
4301
|
+
"Accept-Encoding": "gzip"
|
|
4302
|
+
},
|
|
4303
|
+
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
|
|
4304
|
+
});
|
|
4305
|
+
if (response.status !== 200) {
|
|
4306
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4307
|
+
}
|
|
4308
|
+
const buffer = await response.arrayBuffer();
|
|
4309
|
+
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
|
|
4310
|
+
new Uint8Array(buffer)
|
|
4289
4311
|
);
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4312
|
+
const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
|
|
4313
|
+
enums: String,
|
|
4314
|
+
longs: String,
|
|
4315
|
+
bytes: String,
|
|
4316
|
+
defaults: false,
|
|
4317
|
+
arrays: true,
|
|
4318
|
+
objects: true,
|
|
4319
|
+
oneofs: true
|
|
4320
|
+
});
|
|
4321
|
+
return feedMessage;
|
|
4322
|
+
} catch (error) {
|
|
4323
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4324
|
+
if (attempt === MAX_RETRIES) {
|
|
4325
|
+
if (task.ignoreErrors) {
|
|
4326
|
+
task.logError(
|
|
4327
|
+
`Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${errorMessage}`
|
|
4328
|
+
);
|
|
4329
|
+
return null;
|
|
4330
|
+
}
|
|
4331
|
+
throw error;
|
|
4332
|
+
}
|
|
4333
|
+
task.logWarning(`Attempt ${attempt} failed for ${type}: ${errorMessage}`);
|
|
4334
|
+
await new Promise(
|
|
4335
|
+
(resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
|
|
4336
|
+
);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
return null;
|
|
4340
|
+
}
|
|
4341
|
+
function getUrlConfig(type, task) {
|
|
4342
|
+
switch (type) {
|
|
4343
|
+
case "alerts":
|
|
4344
|
+
return task.realtimeAlerts;
|
|
4345
|
+
case "tripupdates":
|
|
4346
|
+
return task.realtimeTripUpdates;
|
|
4347
|
+
case "vehiclepositions":
|
|
4348
|
+
return task.realtimeVehiclePositions;
|
|
4349
|
+
default:
|
|
4350
|
+
return void 0;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
function createServiceAlertsProcessor(db, task) {
|
|
4354
|
+
const alertStmt = createPreparedStatement(db, serviceAlerts);
|
|
4355
|
+
const informedEntityStmt = createPreparedStatement(
|
|
4356
|
+
db,
|
|
4357
|
+
serviceAlertInformedEntities
|
|
4358
|
+
);
|
|
4359
|
+
return async (batch) => {
|
|
4360
|
+
let recordCount = 0;
|
|
4361
|
+
let errorCount = 0;
|
|
4362
|
+
db.transaction(() => {
|
|
4363
|
+
for (const entity of batch) {
|
|
4364
|
+
try {
|
|
4365
|
+
const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4366
|
+
alertStmt.run(alertValues);
|
|
4367
|
+
recordCount++;
|
|
4368
|
+
if (entity.alert?.informedEntity?.length) {
|
|
4369
|
+
for (const informedEntity of entity.alert.informedEntity) {
|
|
4295
4370
|
informedEntity.parent = entity;
|
|
4296
|
-
|
|
4371
|
+
const entityValues = serviceAlertInformedEntities.schema.map(
|
|
4297
4372
|
(column) => prepareRealtimeFieldValue(informedEntity, column, task)
|
|
4298
4373
|
);
|
|
4374
|
+
informedEntityStmt.run(entityValues);
|
|
4375
|
+
recordCount++;
|
|
4299
4376
|
}
|
|
4300
|
-
);
|
|
4301
|
-
for (const values of informedEntities) {
|
|
4302
|
-
informedEntityStmt.run(values);
|
|
4303
4377
|
}
|
|
4378
|
+
} catch (error) {
|
|
4379
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4380
|
+
errorCount++;
|
|
4381
|
+
task.logWarning(`Alert processing error: ${errorMessage}`);
|
|
4304
4382
|
}
|
|
4305
|
-
totalLineCount++;
|
|
4306
|
-
} catch (error) {
|
|
4307
|
-
task.logWarning(`Import error: ${error.message}`);
|
|
4308
4383
|
}
|
|
4309
|
-
}
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
true
|
|
4313
|
-
);
|
|
4314
|
-
})();
|
|
4384
|
+
})();
|
|
4385
|
+
return { recordCount, errorCount };
|
|
4386
|
+
};
|
|
4315
4387
|
}
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
", "
|
|
4321
|
-
)}) VALUES (${tripUpdates.schema.map(() => "?").join(", ")})`
|
|
4388
|
+
function createTripUpdatesProcessor(db, task) {
|
|
4389
|
+
const tripUpdateStmt = createPreparedStatement(
|
|
4390
|
+
db,
|
|
4391
|
+
tripUpdates
|
|
4322
4392
|
);
|
|
4323
|
-
const stopTimeStmt =
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
)}) VALUES (${stopTimeUpdates.schema.map(() => "?").join(", ")})`
|
|
4393
|
+
const stopTimeStmt = createPreparedStatement(
|
|
4394
|
+
db,
|
|
4395
|
+
stopTimeUpdates
|
|
4327
4396
|
);
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
(
|
|
4339
|
-
|
|
4340
|
-
|
|
4397
|
+
return async (batch) => {
|
|
4398
|
+
let recordCount = 0;
|
|
4399
|
+
let errorCount = 0;
|
|
4400
|
+
db.transaction(() => {
|
|
4401
|
+
for (const entity of batch) {
|
|
4402
|
+
try {
|
|
4403
|
+
const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4404
|
+
tripUpdateStmt.run(tripUpdateValues);
|
|
4405
|
+
recordCount++;
|
|
4406
|
+
if (entity.tripUpdate?.stopTimeUpdate?.length) {
|
|
4407
|
+
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
|
|
4408
|
+
stopTimeUpdate.parent = entity;
|
|
4409
|
+
const stopTimeValues = stopTimeUpdates.schema.map(
|
|
4410
|
+
(column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
|
|
4411
|
+
);
|
|
4412
|
+
stopTimeStmt.run(stopTimeValues);
|
|
4413
|
+
recordCount++;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
} catch (error) {
|
|
4417
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4418
|
+
errorCount++;
|
|
4419
|
+
task.logWarning(`Trip update processing error: ${errorMessage}`);
|
|
4341
4420
|
}
|
|
4342
|
-
totalLineCount++;
|
|
4343
|
-
} catch (error) {
|
|
4344
|
-
task.logWarning(`Import error: ${error.message}`);
|
|
4345
4421
|
}
|
|
4346
|
-
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
true
|
|
4350
|
-
);
|
|
4351
|
-
})();
|
|
4422
|
+
})();
|
|
4423
|
+
return { recordCount, errorCount };
|
|
4424
|
+
};
|
|
4352
4425
|
}
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
", "
|
|
4358
|
-
)}) VALUES (${vehiclePositions.schema.map(() => "?").join(", ")})`
|
|
4426
|
+
function createVehiclePositionsProcessor(db, task) {
|
|
4427
|
+
const vehiclePositionStmt = createPreparedStatement(
|
|
4428
|
+
db,
|
|
4429
|
+
vehiclePositions
|
|
4359
4430
|
);
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4431
|
+
return async (batch) => {
|
|
4432
|
+
let recordCount = 0;
|
|
4433
|
+
let errorCount = 0;
|
|
4434
|
+
db.transaction(() => {
|
|
4435
|
+
for (const entity of batch) {
|
|
4436
|
+
try {
|
|
4437
|
+
const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
|
|
4438
|
+
vehiclePositionStmt.run(fieldValues);
|
|
4439
|
+
recordCount++;
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4442
|
+
errorCount++;
|
|
4443
|
+
task.logWarning(`Vehicle position processing error: ${errorMessage}`);
|
|
4444
|
+
}
|
|
4368
4445
|
}
|
|
4369
|
-
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
true
|
|
4373
|
-
);
|
|
4374
|
-
})();
|
|
4446
|
+
})();
|
|
4447
|
+
return { recordCount, errorCount };
|
|
4448
|
+
};
|
|
4375
4449
|
}
|
|
4376
4450
|
async function updateGtfsRealtimeData(task) {
|
|
4377
|
-
if (task.realtimeAlerts
|
|
4451
|
+
if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
|
|
4378
4452
|
return;
|
|
4379
4453
|
}
|
|
4454
|
+
const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
|
|
4455
|
+
[
|
|
4456
|
+
task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
|
|
4457
|
+
task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
|
|
4458
|
+
task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
|
|
4459
|
+
]
|
|
4460
|
+
);
|
|
4380
4461
|
const db = openDb({ sqlitePath: task.sqlitePath });
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4462
|
+
const recordCounts = {
|
|
4463
|
+
alerts: 0,
|
|
4464
|
+
tripupdates: 0,
|
|
4465
|
+
vehiclepositions: 0
|
|
4466
|
+
};
|
|
4467
|
+
const processingPromises = [];
|
|
4468
|
+
if (alertsData?.entity?.length) {
|
|
4469
|
+
processingPromises.push(
|
|
4470
|
+
processBatch(
|
|
4471
|
+
alertsData.entity,
|
|
4472
|
+
BATCH_SIZE,
|
|
4473
|
+
createServiceAlertsProcessor(db, task)
|
|
4474
|
+
).then((result) => {
|
|
4475
|
+
recordCounts.alerts = result.recordCount;
|
|
4476
|
+
})
|
|
4477
|
+
);
|
|
4394
4478
|
}
|
|
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
|
-
}
|
|
4479
|
+
if (tripUpdatesData?.entity?.length) {
|
|
4480
|
+
processingPromises.push(
|
|
4481
|
+
processBatch(
|
|
4482
|
+
tripUpdatesData.entity,
|
|
4483
|
+
BATCH_SIZE,
|
|
4484
|
+
createTripUpdatesProcessor(db, task)
|
|
4485
|
+
).then((result) => {
|
|
4486
|
+
recordCounts.tripupdates = result.recordCount;
|
|
4487
|
+
})
|
|
4488
|
+
);
|
|
4411
4489
|
}
|
|
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
|
-
}
|
|
4490
|
+
if (vehiclePositionsData?.entity?.length) {
|
|
4491
|
+
processingPromises.push(
|
|
4492
|
+
processBatch(
|
|
4493
|
+
vehiclePositionsData.entity,
|
|
4494
|
+
BATCH_SIZE,
|
|
4495
|
+
createVehiclePositionsProcessor(db, task)
|
|
4496
|
+
).then((result) => {
|
|
4497
|
+
recordCounts.vehiclepositions = result.recordCount;
|
|
4498
|
+
})
|
|
4499
|
+
);
|
|
4428
4500
|
}
|
|
4429
|
-
|
|
4501
|
+
await Promise.all(processingPromises);
|
|
4502
|
+
task.log(
|
|
4503
|
+
`GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
|
|
4504
|
+
);
|
|
4430
4505
|
}
|
|
4431
4506
|
|
|
4432
4507
|
// src/lib/import-gtfs.ts
|
|
@@ -4459,7 +4534,7 @@ var extractGtfsFiles = async (task) => {
|
|
|
4459
4534
|
throw new Error("No `path` specified in config");
|
|
4460
4535
|
}
|
|
4461
4536
|
const gtfsPath = untildify(task.path);
|
|
4462
|
-
task.log(`Importing GTFS from ${task.path}\r`);
|
|
4537
|
+
task.log(`Importing static GTFS from ${task.path}\r`);
|
|
4463
4538
|
if (path2.extname(gtfsPath) === ".zip") {
|
|
4464
4539
|
try {
|
|
4465
4540
|
await unzip(gtfsPath, task.downloadDir);
|
|
@@ -4542,11 +4617,11 @@ var createGtfsTables = (db) => {
|
|
|
4542
4617
|
if (column.type === "time") {
|
|
4543
4618
|
sqlColumnCreateStatements.push(
|
|
4544
4619
|
`${getTimestampColumnName(column.name)} INTEGER GENERATED ALWAYS AS (
|
|
4545
|
-
CASE
|
|
4546
|
-
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
4620
|
+
CASE
|
|
4621
|
+
WHEN ${column.name} IS NULL OR ${column.name} = '' THEN NULL
|
|
4547
4622
|
ELSE CAST(
|
|
4548
|
-
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
4549
|
-
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
4623
|
+
substr(${column.name}, 1, instr(${column.name}, ':') - 1) * 3600 +
|
|
4624
|
+
substr(${column.name}, instr(${column.name}, ':') + 1, 2) * 60 +
|
|
4550
4625
|
substr(${column.name}, -2) AS INTEGER
|
|
4551
4626
|
)
|
|
4552
4627
|
END
|
|
@@ -4603,7 +4678,7 @@ var formatGtfsLine = (line, model, totalLineCount) => {
|
|
|
4603
4678
|
continue;
|
|
4604
4679
|
}
|
|
4605
4680
|
if (type === "date") {
|
|
4606
|
-
value = value.replace(/-/g, "");
|
|
4681
|
+
value = value?.toString().replace(/-/g, "");
|
|
4607
4682
|
if (value.length !== 8) {
|
|
4608
4683
|
throw new Error(
|
|
4609
4684
|
`Invalid date in ${filenameBase}.${filenameExtension} for ${name} on line ${lineNumber}.`
|
|
@@ -4619,203 +4694,212 @@ var formatGtfsLine = (line, model, totalLineCount) => {
|
|
|
4619
4694
|
}
|
|
4620
4695
|
return formattedLine;
|
|
4621
4696
|
};
|
|
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`);
|
|
4697
|
+
var BATCH_SIZE2 = 1e5;
|
|
4698
|
+
var importGtfsFiles = async (db, task) => {
|
|
4699
|
+
await mapSeries2(
|
|
4700
|
+
Object.values(models_exports),
|
|
4701
|
+
(model) => new Promise((resolve, reject) => {
|
|
4702
|
+
let totalLineCount = 0;
|
|
4703
|
+
const filename = `${model.filenameBase}.${model.filenameExtension}`;
|
|
4704
|
+
if (task.exclude && task.exclude.includes(model.filenameBase)) {
|
|
4705
|
+
task.log(`Skipping - ${filename}\r`);
|
|
4706
|
+
resolve();
|
|
4707
|
+
return;
|
|
4641
4708
|
}
|
|
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;
|
|
4709
|
+
if (model.extension === "gtfs-realtime") {
|
|
4710
|
+
resolve();
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
const filepath = path2.join(task.downloadDir, `${filename}`);
|
|
4714
|
+
if (!existsSync2(filepath)) {
|
|
4715
|
+
if (!model.nonstandard) {
|
|
4716
|
+
task.log(`Importing - ${filename} - No file found\r`);
|
|
4685
4717
|
}
|
|
4718
|
+
resolve();
|
|
4719
|
+
return;
|
|
4686
4720
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
const
|
|
4690
|
-
columns
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4721
|
+
task.log(`Importing - ${filename}\r`);
|
|
4722
|
+
const columns = model.schema;
|
|
4723
|
+
const prefixedColumns = new Set(
|
|
4724
|
+
columns.filter((column) => column.prefix).map((column) => column.name)
|
|
4725
|
+
);
|
|
4726
|
+
const prepareStatement = `INSERT ${task.ignoreDuplicates ? "OR IGNORE" : ""} INTO ${model.filenameBase} (${columns.map(({ name }) => name).join(", ")}) VALUES (${columns.map(({ name }) => `@${name}`).join(", ")})`;
|
|
4727
|
+
const insert = db.prepare(prepareStatement);
|
|
4728
|
+
const insertLines = db.transaction((lines) => {
|
|
4729
|
+
for (const [rowNumber, line] of Object.entries(lines)) {
|
|
4730
|
+
try {
|
|
4731
|
+
if (task.prefix === void 0) {
|
|
4732
|
+
insert.run(line);
|
|
4733
|
+
} else {
|
|
4734
|
+
const prefixedLine = Object.fromEntries(
|
|
4735
|
+
Object.entries(
|
|
4736
|
+
line
|
|
4737
|
+
).map(([columnName, value]) => [
|
|
4738
|
+
columnName,
|
|
4739
|
+
applyPrefixToValue(
|
|
4740
|
+
value,
|
|
4741
|
+
prefixedColumns.has(columnName),
|
|
4742
|
+
task.prefix
|
|
4743
|
+
)
|
|
4744
|
+
])
|
|
4709
4745
|
);
|
|
4746
|
+
insert.run(prefixedLine);
|
|
4710
4747
|
}
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4748
|
+
} catch (error) {
|
|
4749
|
+
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
4750
|
+
const primaryColumns = columns.filter(
|
|
4751
|
+
(column) => column.primary
|
|
4752
|
+
);
|
|
4753
|
+
task.logWarning(
|
|
4754
|
+
`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`
|
|
4755
|
+
);
|
|
4756
|
+
}
|
|
4757
|
+
task.logWarning(
|
|
4758
|
+
`Check ${filename} for invalid data on line ${rowNumber + 1}.`
|
|
4759
|
+
);
|
|
4760
|
+
throw error;
|
|
4718
4761
|
}
|
|
4719
4762
|
}
|
|
4720
4763
|
});
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4764
|
+
if (model.filenameExtension === "txt") {
|
|
4765
|
+
const parser = parse({
|
|
4766
|
+
columns: true,
|
|
4767
|
+
relax_quotes: true,
|
|
4768
|
+
trim: true,
|
|
4769
|
+
skip_empty_lines: true,
|
|
4770
|
+
...task.csvOptions
|
|
4771
|
+
});
|
|
4772
|
+
let lines = [];
|
|
4773
|
+
parser.on("readable", () => {
|
|
4774
|
+
try {
|
|
4775
|
+
let record;
|
|
4776
|
+
while (record = parser.read()) {
|
|
4777
|
+
totalLineCount += 1;
|
|
4778
|
+
lines.push(formatGtfsLine(record, model, totalLineCount));
|
|
4779
|
+
if (lines.length >= BATCH_SIZE2) {
|
|
4780
|
+
insertLines(lines);
|
|
4781
|
+
lines = [];
|
|
4782
|
+
task.log(
|
|
4783
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4784
|
+
true
|
|
4730
4785
|
);
|
|
4731
|
-
resolve();
|
|
4732
|
-
return;
|
|
4733
|
-
} else {
|
|
4734
|
-
reject(error);
|
|
4735
|
-
return;
|
|
4736
4786
|
}
|
|
4737
4787
|
}
|
|
4788
|
+
} catch (error) {
|
|
4789
|
+
if (task.ignoreErrors) {
|
|
4790
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4791
|
+
task.logError(`Error processing ${filename}: ${errorMessage}`);
|
|
4792
|
+
resolve();
|
|
4793
|
+
} else {
|
|
4794
|
+
reject(error);
|
|
4795
|
+
}
|
|
4738
4796
|
}
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4797
|
+
});
|
|
4798
|
+
parser.on("end", () => {
|
|
4799
|
+
try {
|
|
4800
|
+
if (lines.length > 0) {
|
|
4801
|
+
try {
|
|
4802
|
+
insertLines(lines);
|
|
4803
|
+
} catch (error) {
|
|
4804
|
+
if (task.ignoreErrors) {
|
|
4805
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4806
|
+
task.logError(
|
|
4807
|
+
`Error inserting data for ${filename}: ${errorMessage}`
|
|
4808
|
+
);
|
|
4809
|
+
resolve();
|
|
4810
|
+
return;
|
|
4811
|
+
} else {
|
|
4812
|
+
reject(error);
|
|
4813
|
+
return;
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
task.log(
|
|
4818
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4819
|
+
true
|
|
4820
|
+
);
|
|
4747
4821
|
resolve();
|
|
4748
|
-
}
|
|
4749
|
-
|
|
4822
|
+
} catch (error) {
|
|
4823
|
+
if (task.ignoreErrors) {
|
|
4824
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4825
|
+
task.logError(`Error finalizing ${filename}: ${errorMessage}`);
|
|
4826
|
+
resolve();
|
|
4827
|
+
} else {
|
|
4828
|
+
reject(error);
|
|
4829
|
+
}
|
|
4750
4830
|
}
|
|
4751
|
-
}
|
|
4752
|
-
|
|
4753
|
-
parser.on("error", (error) => {
|
|
4754
|
-
if (task.ignoreErrors) {
|
|
4755
|
-
task.logError(`Parser error for ${filename}: ${error.message}`);
|
|
4756
|
-
resolve();
|
|
4757
|
-
} else {
|
|
4758
|
-
reject(error);
|
|
4759
|
-
}
|
|
4760
|
-
});
|
|
4761
|
-
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
4762
|
-
} else if (model.filenameExtension === "geojson") {
|
|
4763
|
-
readFile2(filepath, "utf8").then((data) => {
|
|
4764
|
-
if (isValidJSON(data) === false) {
|
|
4831
|
+
});
|
|
4832
|
+
parser.on("error", (error) => {
|
|
4765
4833
|
if (task.ignoreErrors) {
|
|
4766
|
-
|
|
4834
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4835
|
+
task.logError(`Parser error for ${filename}: ${errorMessage}`);
|
|
4767
4836
|
resolve();
|
|
4768
|
-
return;
|
|
4769
4837
|
} else {
|
|
4770
|
-
reject(
|
|
4771
|
-
return;
|
|
4838
|
+
reject(error);
|
|
4772
4839
|
}
|
|
4773
|
-
}
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4840
|
+
});
|
|
4841
|
+
createReadStream(filepath).pipe(stripBomStream()).pipe(parser);
|
|
4842
|
+
} else if (model.filenameExtension === "geojson") {
|
|
4843
|
+
readFile2(filepath, "utf8").then((data) => {
|
|
4844
|
+
if (isValidJSON(data) === false) {
|
|
4845
|
+
if (task.ignoreErrors) {
|
|
4846
|
+
task.logError(`Invalid JSON in ${filename}`);
|
|
4847
|
+
resolve();
|
|
4848
|
+
return;
|
|
4849
|
+
} else {
|
|
4850
|
+
reject(new Error(`Invalid JSON in ${filename}`));
|
|
4851
|
+
return;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
totalLineCount += 1;
|
|
4855
|
+
const line = formatGtfsLine(
|
|
4856
|
+
{ geojson: data },
|
|
4857
|
+
model,
|
|
4858
|
+
totalLineCount
|
|
4785
4859
|
);
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4860
|
+
try {
|
|
4861
|
+
insertLines([line]);
|
|
4862
|
+
task.log(
|
|
4863
|
+
`Importing - ${filename} - ${totalLineCount} lines imported\r`,
|
|
4864
|
+
true
|
|
4791
4865
|
);
|
|
4792
4866
|
resolve();
|
|
4867
|
+
} catch (error) {
|
|
4868
|
+
if (task.ignoreErrors) {
|
|
4869
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4870
|
+
task.logError(
|
|
4871
|
+
`Error inserting data for ${filename}: ${errorMessage}`
|
|
4872
|
+
);
|
|
4873
|
+
resolve();
|
|
4874
|
+
} else {
|
|
4875
|
+
reject(error);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
}).catch((error) => {
|
|
4879
|
+
if (task.ignoreErrors) {
|
|
4880
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4881
|
+
task.logError(`Error reading ${filename}: ${errorMessage}`);
|
|
4882
|
+
resolve();
|
|
4793
4883
|
} else {
|
|
4794
4884
|
reject(error);
|
|
4795
4885
|
}
|
|
4796
|
-
}
|
|
4797
|
-
}
|
|
4886
|
+
});
|
|
4887
|
+
} else {
|
|
4798
4888
|
if (task.ignoreErrors) {
|
|
4799
|
-
task.logError(
|
|
4889
|
+
task.logError(
|
|
4890
|
+
`Unsupported file type: ${model.filenameExtension} for ${filename}`
|
|
4891
|
+
);
|
|
4800
4892
|
resolve();
|
|
4801
4893
|
} else {
|
|
4802
|
-
reject(
|
|
4894
|
+
reject(
|
|
4895
|
+
new Error(`Unsupported file type: ${model.filenameExtension}`)
|
|
4896
|
+
);
|
|
4803
4897
|
}
|
|
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
4898
|
}
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
);
|
|
4899
|
+
})
|
|
4900
|
+
);
|
|
4901
|
+
task.log(`Static GTFS import complete`);
|
|
4902
|
+
};
|
|
4819
4903
|
async function importGtfs(initialConfig) {
|
|
4820
4904
|
const startTime = process.hrtime.bigint();
|
|
4821
4905
|
const config = setDefaultConfig(initialConfig);
|
|
@@ -4832,7 +4916,6 @@ async function importGtfs(initialConfig) {
|
|
|
4832
4916
|
const tempPath = temporaryDirectory();
|
|
4833
4917
|
const task = {
|
|
4834
4918
|
exclude: agency2.exclude,
|
|
4835
|
-
url: agency2.url,
|
|
4836
4919
|
headers: agency2.headers,
|
|
4837
4920
|
realtimeAlerts: agency2.realtimeAlerts,
|
|
4838
4921
|
realtimeTripUpdates: agency2.realtimeTripUpdates,
|
|
@@ -4840,7 +4923,6 @@ async function importGtfs(initialConfig) {
|
|
|
4840
4923
|
downloadDir: tempPath,
|
|
4841
4924
|
downloadTimeout: config.downloadTimeout,
|
|
4842
4925
|
gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
|
|
4843
|
-
path: agency2.path,
|
|
4844
4926
|
csvOptions: config.csvOptions || {},
|
|
4845
4927
|
ignoreDuplicates: config.ignoreDuplicates,
|
|
4846
4928
|
ignoreErrors: config.ignoreErrors,
|
|
@@ -4851,8 +4933,13 @@ async function importGtfs(initialConfig) {
|
|
|
4851
4933
|
logWarning: logWarning(config),
|
|
4852
4934
|
logError: logError(config)
|
|
4853
4935
|
};
|
|
4854
|
-
if (
|
|
4936
|
+
if ("url" in agency2) {
|
|
4937
|
+
Object.assign(task, { url: agency2.url });
|
|
4855
4938
|
await downloadGtfsFiles(task);
|
|
4939
|
+
} else {
|
|
4940
|
+
Object.assign(task, {
|
|
4941
|
+
path: agency2.path
|
|
4942
|
+
});
|
|
4856
4943
|
}
|
|
4857
4944
|
await extractGtfsFiles(task);
|
|
4858
4945
|
await importGtfsFiles(db, task);
|
|
@@ -4860,7 +4947,8 @@ async function importGtfs(initialConfig) {
|
|
|
4860
4947
|
await rm2(tempPath, { recursive: true });
|
|
4861
4948
|
} catch (error) {
|
|
4862
4949
|
if (config.ignoreErrors) {
|
|
4863
|
-
|
|
4950
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4951
|
+
logError(config)(errorMessage);
|
|
4864
4952
|
} else {
|
|
4865
4953
|
throw error;
|
|
4866
4954
|
}
|
|
@@ -4875,7 +4963,7 @@ async function importGtfs(initialConfig) {
|
|
|
4875
4963
|
`
|
|
4876
4964
|
);
|
|
4877
4965
|
} catch (error) {
|
|
4878
|
-
if (error
|
|
4966
|
+
if (error.code === "SQLITE_CANTOPEN") {
|
|
4879
4967
|
logError(config)(
|
|
4880
4968
|
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
4881
4969
|
);
|