gtfs
Version:
Import GTFS transit data into SQLite and query routes, stops, times, fares and more
1,190 lines (1,165 loc) • 32.1 kB
JavaScript
// src/bin/gtfsrealtime-update.ts
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import PrettyError from "pretty-error";
// src/lib/file-utils.ts
import path from "path";
import { existsSync } from "fs";
import { homedir } from "os";
import { mkdir, readFile, rm } from "fs/promises";
import { omit, snakeCase } from "lodash-es";
import sanitize from "sanitize-filename";
import StreamZip from "node-stream-zip";
// src/lib/log-utils.ts
import { clearLine, cursorTo } from "readline";
import { noop } from "lodash-es";
import * as colors from "yoctocolors";
function log(config) {
if (config.verbose === false) {
return noop;
}
if (config.logFunction) {
return config.logFunction;
}
return (text, overwrite = false) => {
if (overwrite && process.stdout.isTTY) {
clearLine(process.stdout, 0);
cursorTo(process.stdout, 0);
} else {
process.stdout.write("\n");
}
process.stdout.write(text);
};
}
function logWarning(config) {
if (config.logFunction) {
return config.logFunction;
}
return (text) => {
process.stdout.write(`
${formatWarning(text)}
`);
};
}
function logError(config) {
if (config.logFunction) {
return config.logFunction;
}
return (text) => {
process.stdout.write(`
${formatError(text)}
`);
};
}
function formatWarning(text) {
return colors.yellow(`${colors.underline("Warning")}: ${text}`);
}
function formatError(error) {
const messageText = error instanceof Error ? error.message : error;
const cleanMessage = messageText.replace(/^Error:\s*/i, "");
return colors.red(`${colors.underline("Error")}: ${cleanMessage}`);
}
// src/lib/file-utils.ts
var homeDirectory = homedir();
async function getConfig(argv2) {
let config;
let data;
try {
if (argv2.configPath) {
const configPath = path.resolve(untildify(argv2.configPath));
data = await readFile(configPath, "utf8");
config = Object.assign(JSON.parse(data), argv2);
} else if (argv2.gtfsPath || argv2.gtfsUrl || argv2.sqlitePath) {
const agencies = [
...argv2.gtfsPath ? [{ path: argv2.gtfsPath }] : [],
...argv2.gtfsUrl ? [{ url: argv2.gtfsUrl }] : []
];
config = {
agencies,
...omit(argv2, ["path", "url"])
};
} else if (existsSync(path.resolve("./config.json"))) {
data = await readFile(path.resolve("./config.json"), "utf8");
config = Object.assign(JSON.parse(data), argv2);
log(config)("Using configuration from ./config.json");
} else {
throw new Error(
"Cannot find configuration file. Use config-sample.json as a starting point, pass --configPath option."
);
}
return config;
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(
`Cannot parse configuration file. Check to ensure that it is valid JSON. Error: ${error.message}`,
{ cause: error }
);
}
throw error;
}
}
function untildify(pathWithTilde) {
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
}
// src/lib/import-gtfs.ts
import path2 from "path";
import { createReadStream, existsSync as existsSync2, lstatSync } from "fs";
import { cp, readdir, rename, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
import { parse } from "csv-parse";
import stripBomStream from "strip-bom-stream";
import { temporaryDirectory } from "tempy";
import mapSeries2 from "promise-map-series";
// src/models/gtfs-realtime/trip-updates.ts
var tripUpdates = {
filenameBase: "trip_updates",
extension: "gtfs-realtime",
schema: [
{
name: "id",
type: "text",
required: true,
primary: true,
index: true,
source: "id",
prefix: true
},
{
name: "vehicle_id",
type: "text",
index: true,
source: "tripUpdate.vehicle.id",
default: null,
prefix: true
},
{
name: "trip_id",
type: "text",
index: true,
source: "tripUpdate.trip.tripId",
default: null,
prefix: true
},
{
name: "trip_start_time",
type: "text",
source: "tripUpdate.trip.startTime",
default: null
},
{
name: "direction_id",
type: "integer",
source: "tripUpdate.trip.directionId",
default: null
},
{
name: "route_id",
type: "text",
index: true,
source: "tripUpdate.trip.routeId",
default: null,
prefix: true
},
{
name: "start_date",
type: "text",
source: "tripUpdate.trip.startDate",
default: null
},
{
name: "timestamp",
type: "text",
source: "tripUpdate.timestamp",
default: null
},
{
name: "schedule_relationship",
type: "text",
source: "tripUpdate.trip.scheduleRelationship",
default: null
},
{
name: "created_timestamp",
type: "integer",
required: true
},
{
name: "expiration_timestamp",
type: "integer",
required: true
}
]
};
// src/models/gtfs-realtime/stop-time-updates.ts
var stopTimeUpdates = {
filenameBase: "stop_time_updates",
extension: "gtfs-realtime",
schema: [
{
name: "trip_id",
type: "text",
index: true,
source: "parent.tripUpdate.trip.tripId",
default: null,
prefix: true
},
{
name: "trip_start_time",
type: "text",
source: "parent.tripUpdate.trip.startTime",
default: null
},
{
name: "direction_id",
type: "integer",
source: "parent.tripUpdate.trip.directionId",
default: null
},
{
name: "route_id",
type: "text",
index: true,
source: "parent.tripUpdate.trip.routeId",
default: null,
prefix: true
},
{
name: "stop_id",
type: "text",
index: true,
source: "stopId",
default: null,
prefix: true
},
{
name: "stop_sequence",
type: "integer",
source: "stopSequence",
default: null
},
{
name: "arrival_delay",
type: "integer",
source: "arrival.delay",
default: null
},
{
name: "departure_delay",
type: "integer",
source: "departure.delay",
default: null
},
{
name: "departure_timestamp",
type: "text",
source: "departure.time",
default: null
},
{
name: "arrival_timestamp",
type: "text",
source: "arrival.time",
default: null
},
{
name: "schedule_relationship",
type: "text",
source: "scheduleRelationship",
default: null
},
{
name: "created_timestamp",
type: "integer",
required: true
},
{
name: "expiration_timestamp",
type: "integer",
required: true
}
]
};
// src/models/gtfs-realtime/vehicle-positions.ts
var vehiclePositions = {
filenameBase: "vehicle_positions",
extension: "gtfs-realtime",
schema: [
{
name: "id",
type: "text",
required: true,
primary: true,
index: true,
source: "id",
prefix: true
},
{
name: "bearing",
type: "real",
source: "vehicle.position.bearing",
default: null
},
{
name: "latitude",
type: "real",
min: -90,
max: 90,
source: "vehicle.position.latitude",
default: null
},
{
name: "longitude",
type: "real",
source: "vehicle.position.longitude",
min: -180,
max: 180,
default: null
},
{
name: "speed",
type: "real",
min: 0,
source: "vehicle.position.speed",
default: null
},
{
name: "current_stop_sequence",
type: "integer",
source: "vehicle.currentStopSequence",
default: null
},
{
name: "trip_id",
type: "text",
index: true,
source: "vehicle.trip.tripId",
default: null,
prefix: true
},
{
name: "trip_start_date",
type: "text",
index: true,
source: "vehicle.trip.startDate",
default: null
},
{
name: "trip_start_time",
type: "text",
index: true,
source: "vehicle.trip.startTime",
default: null
},
{
name: "congestion_level",
type: "text",
source: "vehicle.congestionLevel",
default: null
},
{
name: "occupancy_status",
type: "text",
source: "vehicle.occupancyStatus",
default: null
},
{
name: "occupancy_percentage",
type: "integer",
source: "vehicle.occupancyPercentage",
default: null
},
{
name: "vehicle_stop_status",
type: "text",
source: "vehicle.vehicleStopStatus",
default: null
},
{
name: "vehicle_id",
type: "text",
index: true,
source: "vehicle.vehicle.id",
default: null,
prefix: true
},
{
name: "vehicle_label",
type: "text",
source: "vehicle.vehicle.label",
default: null
},
{
name: "vehicle_license_plate",
type: "text",
source: "vehicle.vehicle.licensePlate",
default: null
},
{
name: "vehicle_wheelchair_accessible",
type: "text",
source: "vehicle.vehicle.wheelchairAccessible",
default: null
},
{
name: "timestamp",
type: "text",
source: "vehicle.timestamp",
default: null
},
{
name: "created_timestamp",
type: "integer",
required: true
},
{
name: "expiration_timestamp",
type: "integer",
required: true
}
]
};
// src/models/gtfs-realtime/service-alerts.ts
var serviceAlerts = {
filenameBase: "service_alerts",
extension: "gtfs-realtime",
schema: [
{
name: "id",
type: "text",
required: true,
primary: true,
index: true,
source: "id",
prefix: true
},
{
name: "active_period",
type: "json",
source: "alert.activePeriod"
},
{
name: "cause",
type: "text",
source: "alert.cause"
},
{
name: "effect",
type: "text",
source: "alert.effect"
},
{
name: "url",
type: "text",
source: "alert.url.translation[0].text",
default: ""
},
{
name: "start_time",
type: "text",
required: true,
source: "alert.activePeriod[0].start",
default: ""
},
{
name: "end_time",
type: "text",
required: true,
source: "alert.activePeriod[0].end",
default: ""
},
{
name: "header_text",
type: "text",
required: true,
source: "alert.headerText.translation[0].text",
default: ""
},
{
name: "description_text",
type: "text",
required: true,
source: "alert.descriptionText.translation[0].text",
default: ""
},
{
name: "tts_header_text",
type: "text",
source: "alert.ttsHeaderText.translation[0].text"
},
{
name: "tts_description_text",
type: "text",
source: "alert.ttsDescriptionText.translation[0].text"
},
{
name: "severity_level",
type: "text",
source: "alert.severityLevel"
},
{
name: "created_timestamp",
type: "integer",
required: true
},
{
name: "expiration_timestamp",
type: "integer",
required: true
}
]
};
// src/models/gtfs-realtime/service-alert-informed_entities.ts
var serviceAlertInformedEntities = {
filenameBase: "service_alert_informed_entities",
extension: "gtfs-realtime",
schema: [
{
name: "alert_id",
type: "text",
required: true,
primary: true,
source: "parent.id",
prefix: true
},
{
name: "stop_id",
type: "text",
index: true,
source: "stopId",
default: null,
prefix: true
},
{
name: "route_id",
type: "text",
index: true,
source: "routeId",
default: null,
prefix: true
},
{
name: "route_type",
type: "integer",
index: true,
source: "routeType",
default: null
},
{
name: "trip_id",
type: "text",
index: true,
source: "trip.tripId",
default: null,
prefix: true
},
{
name: "direction_id",
type: "integer",
index: true,
source: "directionId",
default: null
},
{
name: "created_timestamp",
type: "integer",
required: true
},
{
name: "expiration_timestamp",
type: "integer",
required: true
}
]
};
// src/lib/db.ts
import Database from "better-sqlite3";
// src/lib/errors.ts
var GtfsError = class extends Error {
code;
category;
isOperational;
statusCode;
details;
constructor(message, options) {
super(message, { cause: options.cause });
this.name = "GtfsError";
this.code = options.code;
this.category = options.category;
this.isOperational = options.isOperational ?? true;
this.statusCode = options.statusCode;
this.details = options.details;
}
};
function isGtfsError(error) {
if (!error || typeof error !== "object") {
return false;
}
const candidate = error;
return candidate.name === "GtfsError" && typeof candidate.message === "string" && typeof candidate.code === "string" && typeof candidate.category === "string" && typeof candidate.isOperational === "boolean";
}
function toGtfsError(error, fallback) {
if (isGtfsError(error)) {
return error;
}
return new GtfsError(fallback.message, {
...fallback,
cause: error
});
}
function addImportError(report, error) {
report.errors.push(error);
report.errorCountsByCode[error.code] = (report.errorCountsByCode[error.code] ?? 0) + 1;
}
function formatGtfsError(error, options = { verbosity: "developer" }) {
if (!isGtfsError(error)) {
const message = error instanceof Error ? error.message : String(error);
return options.verbosity === "user" ? message : `UNKNOWN_ERROR: ${message}`;
}
if (options.verbosity === "user") {
return error.message;
}
return [
`${error.code}: ${error.message}`,
`category=${error.category}`,
error.statusCode !== void 0 ? `statusCode=${error.statusCode}` : null,
error.details ? `details=${JSON.stringify(error.details)}` : null
].filter(Boolean).join(" | ");
}
// src/lib/db.ts
var dbs = {};
function setupDb(sqlitePath) {
const db = new Database(untildify(sqlitePath));
db.pragma("journal_mode = OFF");
db.pragma("synchronous = OFF");
db.pragma("temp_store = MEMORY");
dbs[sqlitePath] = db;
return db;
}
function openDb(config = null) {
if (config) {
const { sqlitePath = ":memory:", db } = config;
if (db) {
return db;
}
if (dbs[sqlitePath]) {
return dbs[sqlitePath];
}
return setupDb(sqlitePath);
}
if (Object.keys(dbs).length === 0) {
return setupDb(":memory:");
}
if (Object.keys(dbs).length === 1) {
const filename = Object.keys(dbs)[0];
return dbs[filename];
}
if (Object.keys(dbs).length > 1) {
throw new GtfsError(
"Multiple databases open, please specify which one to use.",
{
code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
category: "database" /* DATABASE */,
details: { openDatabaseCount: Object.keys(dbs).length }
}
);
}
throw new GtfsError("Unable to find database connection.", {
code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
category: "database" /* DATABASE */
});
}
// src/lib/geojson-utils.ts
import {
cloneDeep,
compact,
filter,
groupBy,
last,
omit as omit2,
sortBy,
omitBy
} from "lodash-es";
import { feature, featureCollection } from "@turf/helpers";
// src/lib/import-gtfs-realtime.ts
import GtfsRealtimeBindings from "gtfs-realtime-bindings";
import mapSeries from "promise-map-series";
import { get } from "lodash-es";
// src/lib/utils.ts
import sqlString from "sqlstring-sqlite";
import Long from "long";
function validateConfigForImport(config) {
if (!config.agencies || config.agencies.length === 0) {
throw new GtfsError("No `agencies` specified in config", {
code: "GTFS_CONFIG_INVALID" /* GTFS_CONFIG_INVALID */,
category: "config" /* CONFIG */,
details: { field: "agencies" }
});
}
for (const [index, agency2] of config.agencies.entries()) {
if (!agency2.path && !agency2.url) {
throw new GtfsError(
`No Agency \`url\` or \`path\` specified in config for agency index ${index}.`,
{
code: "GTFS_CONFIG_INVALID" /* GTFS_CONFIG_INVALID */,
category: "config" /* CONFIG */,
details: { agencyIndex: index }
}
);
}
}
return config;
}
function setDefaultConfig(initialConfig) {
const defaults = {
sqlitePath: ":memory:",
ignoreDuplicates: false,
ignoreErrors: false,
gtfsRealtimeExpirationSeconds: 0,
verbose: true,
downloadTimeout: 3e4
};
return {
...defaults,
...initialConfig
};
}
function convertLongTimeToDate(longDate) {
const { high, low, unsigned } = longDate;
return new Date(
Long.fromBits(low, high, unsigned).toNumber() * 1e3
).toISOString();
}
function applyPrefixToValue(value, columnShouldBePrefixed, prefix) {
if (!columnShouldBePrefixed || prefix === void 0 || value === null) {
return value;
}
return `${prefix}${value}`;
}
function pluralize(singularWord, pluralWord, count) {
return count === 1 ? singularWord : pluralWord;
}
// src/lib/import-gtfs-realtime.ts
var BATCH_SIZE = 1e3;
var MAX_RETRIES = 3;
var RETRY_DELAY = 1e3;
function prepareRealtimeFieldValue(entity, column, task) {
if (column.name === "created_timestamp") {
return task.currentTimestamp;
}
if (column.name === "expiration_timestamp") {
return task.currentTimestamp + task.gtfsRealtimeExpirationSeconds;
}
const baseValue = column.source === void 0 ? column.default : get(entity, column.source, column.default);
const timeAdjustedValue = baseValue?.__isLong__ ? convertLongTimeToDate(baseValue) : baseValue;
const prefixedValue = applyPrefixToValue(
timeAdjustedValue,
column.prefix,
task.prefix
);
return column.type === "json" ? JSON.stringify(prefixedValue) : prefixedValue;
}
function createPreparedStatement(db, model) {
const columns = model.schema.map((column) => column.name);
const placeholders = model.schema.map(() => "?").join(", ");
return db.prepare(
`REPLACE INTO ${model.filenameBase} (${columns.join(", ")}) VALUES (${placeholders})`
);
}
async function processBatch(items, batchSize, processor) {
let totalRecordCount = 0;
let totalErrorCount = 0;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
try {
const result = await processor(batch);
totalRecordCount += result.recordCount;
totalErrorCount += result.errorCount;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
totalErrorCount += batch.length;
console.error(`Batch processing error: ${errorMessage}`);
}
}
return { recordCount: totalRecordCount, errorCount: totalErrorCount };
}
async function fetchGtfsRealtimeData(type, task) {
const urlConfig = getUrlConfig(type, task);
if (!urlConfig) {
return null;
}
task.log(`Importing - GTFS-Realtime from ${urlConfig.url}`);
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await fetch(urlConfig.url, {
method: "GET",
headers: {
...urlConfig.headers ?? {},
"Accept-Encoding": "gzip"
},
signal: task.downloadTimeout ? AbortSignal.timeout(task.downloadTimeout) : void 0
});
if (response.status !== 200) {
throw new GtfsError(`HTTP ${response.status}: ${response.statusText}`, {
code: "GTFS_DOWNLOAD_HTTP" /* GTFS_DOWNLOAD_HTTP */,
category: "download" /* DOWNLOAD */,
statusCode: response.status,
details: {
url: urlConfig.url,
status: response.status,
statusText: response.statusText
}
});
}
const buffer = await response.arrayBuffer();
const message = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(
new Uint8Array(buffer)
);
const feedMessage = GtfsRealtimeBindings.transit_realtime.FeedMessage.toObject(message, {
enums: String,
longs: String,
bytes: String,
defaults: false,
arrays: true,
objects: true,
oneofs: true
});
return feedMessage;
} catch (error) {
const gtfsError = toGtfsError(error, {
message: error instanceof Error ? error.message : String(error),
code: "GTFS_DOWNLOAD_FAILED" /* GTFS_DOWNLOAD_FAILED */,
category: "download" /* DOWNLOAD */,
details: { type, url: urlConfig.url }
});
if (attempt === MAX_RETRIES) {
if (task.ignoreErrors) {
task.logError(
`Failed to fetch ${type} after ${MAX_RETRIES} attempts: ${gtfsError.message}`
);
if (task.report) {
addImportError(task.report, gtfsError);
}
return null;
}
throw gtfsError;
}
task.logWarning(
`Attempt ${attempt} failed for ${type}: ${gtfsError.message}`
);
await new Promise(
(resolve) => setTimeout(resolve, RETRY_DELAY * attempt)
);
}
}
return null;
}
function getUrlConfig(type, task) {
switch (type) {
case "alerts":
return task.realtimeAlerts;
case "tripupdates":
return task.realtimeTripUpdates;
case "vehiclepositions":
return task.realtimeVehiclePositions;
default:
return void 0;
}
}
function createServiceAlertsProcessor(db, task) {
const alertStmt = createPreparedStatement(db, serviceAlerts);
const informedEntityStmt = createPreparedStatement(
db,
serviceAlertInformedEntities
);
return async (batch) => {
let recordCount = 0;
let errorCount = 0;
db.transaction(() => {
for (const entity of batch) {
try {
const alertValues = serviceAlerts.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
alertStmt.run(alertValues);
recordCount++;
if (entity.alert?.informedEntity?.length) {
for (const informedEntity of entity.alert.informedEntity) {
informedEntity.parent = entity;
const entityValues = serviceAlertInformedEntities.schema.map(
(column) => prepareRealtimeFieldValue(informedEntity, column, task)
);
informedEntityStmt.run(entityValues);
recordCount++;
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errorCount++;
task.logWarning(`Alert processing error: ${errorMessage}`);
}
}
})();
return { recordCount, errorCount };
};
}
function createTripUpdatesProcessor(db, task) {
const tripUpdateStmt = createPreparedStatement(
db,
tripUpdates
);
const stopTimeStmt = createPreparedStatement(
db,
stopTimeUpdates
);
return async (batch) => {
let recordCount = 0;
let errorCount = 0;
db.transaction(() => {
for (const entity of batch) {
try {
const tripUpdateValues = tripUpdates.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
tripUpdateStmt.run(tripUpdateValues);
recordCount++;
if (entity.tripUpdate?.stopTimeUpdate?.length) {
for (const stopTimeUpdate of entity.tripUpdate.stopTimeUpdate) {
stopTimeUpdate.parent = entity;
const stopTimeValues = stopTimeUpdates.schema.map(
(column) => prepareRealtimeFieldValue(stopTimeUpdate, column, task)
);
stopTimeStmt.run(stopTimeValues);
recordCount++;
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errorCount++;
task.logWarning(`Trip update processing error: ${errorMessage}`);
}
}
})();
return { recordCount, errorCount };
};
}
function createVehiclePositionsProcessor(db, task) {
const vehiclePositionStmt = createPreparedStatement(
db,
vehiclePositions
);
return async (batch) => {
let recordCount = 0;
let errorCount = 0;
db.transaction(() => {
for (const entity of batch) {
try {
const fieldValues = vehiclePositions.schema.map((column) => prepareRealtimeFieldValue(entity, column, task));
vehiclePositionStmt.run(fieldValues);
recordCount++;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errorCount++;
task.logWarning(`Vehicle position processing error: ${errorMessage}`);
}
}
})();
return { recordCount, errorCount };
};
}
function removeExpiredRealtimeData(config) {
const db = openDb(config);
log(config)(`Removing expired GTFS-Realtime data`);
db.transaction(() => {
const tables = [
"vehicle_positions",
"trip_updates",
"stop_time_updates",
"service_alerts",
"service_alert_informed_entities"
];
for (const table of tables) {
db.prepare(
`DELETE FROM ${table} WHERE expiration_timestamp <= strftime('%s','now')`
).run();
}
})();
log(config)(`Removed expired GTFS-Realtime data\r`, true);
}
async function updateGtfsRealtimeData(task) {
if (!task.realtimeAlerts && !task.realtimeTripUpdates && !task.realtimeVehiclePositions) {
return;
}
const [alertsData, tripUpdatesData, vehiclePositionsData] = await Promise.all(
[
task.realtimeAlerts?.url ? fetchGtfsRealtimeData("alerts", task) : null,
task.realtimeTripUpdates?.url ? fetchGtfsRealtimeData("tripupdates", task) : null,
task.realtimeVehiclePositions?.url ? fetchGtfsRealtimeData("vehiclepositions", task) : null
]
);
const db = openDb({ sqlitePath: task.sqlitePath });
const recordCounts = {
alerts: 0,
tripupdates: 0,
vehiclepositions: 0
};
const processingPromises = [];
if (alertsData?.entity?.length) {
processingPromises.push(
processBatch(
alertsData.entity,
BATCH_SIZE,
createServiceAlertsProcessor(db, task)
).then((result) => {
recordCounts.alerts = result.recordCount;
})
);
}
if (tripUpdatesData?.entity?.length) {
processingPromises.push(
processBatch(
tripUpdatesData.entity,
BATCH_SIZE,
createTripUpdatesProcessor(db, task)
).then((result) => {
recordCounts.tripupdates = result.recordCount;
})
);
}
if (vehiclePositionsData?.entity?.length) {
processingPromises.push(
processBatch(
vehiclePositionsData.entity,
BATCH_SIZE,
createVehiclePositionsProcessor(db, task)
).then((result) => {
recordCounts.vehiclepositions = result.recordCount;
})
);
}
await Promise.all(processingPromises);
task.log(
`GTFS-Realtime import complete: ${recordCounts.alerts} alerts, ${recordCounts.tripupdates} trip updates, ${recordCounts.vehiclepositions} vehicle positions`
);
}
async function updateGtfsRealtime(initialConfig) {
const config = setDefaultConfig(initialConfig);
validateConfigForImport(config);
try {
openDb(config);
const agencyCount = config.agencies.length;
log(config)(
`Starting GTFS-Realtime refresh for ${pluralize(
"agency",
"agencies",
agencyCount
)} using SQLite database at ${config.sqlitePath}`
);
removeExpiredRealtimeData(config);
await mapSeries(config.agencies, async (agency2) => {
let task;
try {
task = {
realtimeAlerts: agency2.realtimeAlerts,
realtimeTripUpdates: agency2.realtimeTripUpdates,
realtimeVehiclePositions: agency2.realtimeVehiclePositions,
downloadTimeout: config.downloadTimeout,
gtfsRealtimeExpirationSeconds: config.gtfsRealtimeExpirationSeconds,
ignoreErrors: config.ignoreErrors,
sqlitePath: config.sqlitePath,
prefix: agency2.prefix,
currentTimestamp: Math.floor(Date.now() / 1e3),
log: log(config),
logWarning: logWarning(config),
logError: logError(config)
};
await updateGtfsRealtimeData(task);
} catch (error) {
const gtfsError = toGtfsError(error, {
message: error instanceof Error ? error.message : String(error),
code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
category: "database" /* DATABASE */,
details: { sqlitePath: task?.sqlitePath ?? config.sqlitePath }
});
if (config.ignoreErrors) {
logError(config)(formatGtfsError(gtfsError));
if (task?.report) {
addImportError(task.report, gtfsError);
}
} else {
throw gtfsError;
}
}
});
log(config)(
`Completed GTFS-Realtime refresh for ${pluralize(
"agency",
"agencies",
agencyCount
)}
`
);
} catch (error) {
if (error.code === "SQLITE_CANTOPEN") {
const dbOpenError = new GtfsError(
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
{
code: "DB_OPEN_FAILED" /* DB_OPEN_FAILED */,
category: "database" /* DATABASE */,
details: {
sqlitePath: config.sqlitePath,
dbCode: error.code
},
cause: error
}
);
logError(config)(dbOpenError.message);
throw dbOpenError;
}
throw toGtfsError(error, {
message: error instanceof Error ? error.message : String(error),
code: "GTFS_DB_OPERATION_FAILED" /* GTFS_DB_OPERATION_FAILED */,
category: "database" /* DATABASE */
});
}
}
// src/lib/export.ts
import path3 from "path";
import { writeFile as writeFile2 } from "fs/promises";
import { without, compact as compact2 } from "lodash-es";
import { stringify } from "csv-stringify";
import sqlString2 from "sqlstring-sqlite";
import mapSeries3 from "promise-map-series";
// src/lib/advancedQuery.ts
import sqlString3 from "sqlstring-sqlite";
// src/lib/gtfs/routes.ts
import { omit as omit3, pick } from "lodash-es";
// src/lib/gtfs/shapes.ts
import { compact as compact3, omit as omit4, pick as pick2 } from "lodash-es";
import { featureCollection as featureCollection2 } from "@turf/helpers";
// src/lib/gtfs/stops.ts
import { omit as omit5, orderBy, pick as pick3 } from "lodash-es";
// src/lib/gtfs/stop-times.ts
import { omit as omit6 } from "lodash-es";
import sqlString4 from "sqlstring-sqlite";
// src/lib/gtfs/trips.ts
import { omit as omit7 } from "lodash-es";
import sqlString5 from "sqlstring-sqlite";
// src/bin/gtfsrealtime-update.ts
var pe = new PrettyError();
var argv = yargs(hideBin(process.argv)).usage("Usage: $0 --configPath ./config.json").help().option("c", {
alias: "configPath",
describe: "Path to config file",
type: "string"
}).default("configPath", void 0).parseSync();
var handleError = (error = "Unknown Error") => {
process.stdout.write(`
${formatError(error)}
`);
console.error(pe.render(error));
process.exit(1);
};
var setupImport = async () => {
const config = await getConfig({
configPath: argv.configPath
});
await updateGtfsRealtime(config);
process.exit();
};
setupImport().catch(handleError);
//# sourceMappingURL=gtfsrealtime-update.js.map