gtfs-via-postgres
Version:
Process GTFS using PostgreSQL.
227 lines (217 loc) • 8.86 kB
JavaScript
#!/usr/bin/env node
'use strict'
const {parseArgs} = require('util')
const pkg = require('./package.json')
const {
values: flags,
positionals: args,
} = parseArgs({
options: {
'help': {
type: 'boolean',
short: 'h',
},
'version': {
type: 'boolean',
short: 'v',
},
'silent': {
type: 'boolean',
short: 's',
},
'require-dependencies': {
type: 'boolean',
short: 'd',
},
'ignore-unsupported': {
type: 'boolean',
short: 'u',
},
'route-types-scheme': {
type: 'string',
},
'trips-without-shape-id': {
type: 'boolean',
},
'routes-without-agency-id': {
type: 'boolean',
},
'stops-without-level-id': {
type: 'boolean',
},
'lower-case-lang-codes': {
type: 'boolean',
},
'stops-location-index': {
type: 'boolean',
},
'stats-by-route-date': {
type: 'string',
},
'stats-by-agency-route-stop-hour': {
type: 'string',
},
'stats-active-trips-by-hour': {
type: 'string',
},
'schema': {
type: 'string',
},
'postgraphile': {
type: 'boolean',
},
'postgraphile-password': {
type: 'string',
},
'postgrest': {
type: 'boolean',
},
'postgrest-password': {
type: 'string',
},
'postgrest-query-cost-limit': {
type: 'string',
},
'import-metadata': {
type: 'boolean',
}
},
allowPositionals: true,
})
if (flags.help) {
process.stdout.write(`
Usage:
gtfs-to-sql [options] [--] <gtfs-file> ...
Options:
--silent -s Don't show files being converted.
--require-dependencies -d Require files that the specified GTFS files depend
on to be specified as well (e.g. stop_times.txt
requires trips.txt). Default: false
--ignore-unsupported -u Ignore unsupported files. Default: false
--route-types-scheme Set of route_type values to support.
- basic: core route types in the GTFS spec
- google-extended: Extended GTFS Route Types [1]
- tpeg-pti: proposed TPEG-PTI-based route types [2]
May also be a set of these schemes, separated by \`,\`.
Default: google-extended
--trips-without-shape-id Don't require trips.txt items to have a shape_id.
Default if shapes.txt has not been provided.
--routes-without-agency-id Don't require routes.txt items to have an agency_id.
--stops-without-level-id Don't require stops.txt items to have a level_id.
Default if levels.txt has not been provided.
--stops-location-index Create a spatial index on stops.stop_loc for efficient
queries by geolocation.
--lower-case-lang-codes Accept Language Codes (e.g. in feed_info.feed_lang)
with a different casing than the official BCP-47
language tags (as specified by the GTFS spec),
by lower-casing all of them before validating.
http://www.rfc-editor.org/rfc/bcp/bcp47.txt
http://www.w3.org/International/articles/language-tags/
--stats-by-route-date Wether to generate a stats_by_route_date view
letting you analyze all data per routes and/or date:
- none: Don't generate a view.
- view: Fast generation, slow access.
- materialized-view: Slow generation, fast access.
Default: none
--stats-by-agency-route-stop-hour
Generate a view letting you analyze arrivals/
departures per route, stop and hour.
The flag works like --stats-by-route-date.
--stats-active-trips-by-hour Generate a view letting you analyze the number of
currently running trips over time, by hour.
Like --stats-by-route-date, this flag accepts
none, view & materialized-view.
--schema The schema to use for the database. Default: public
Even when importing into a schema other than \`public\`,
a function \`public.gtfs_via_postgres_import_version()\`
gets created, to ensure that multiple imports into the
same database are all made using the same version. See
also multiple-datasets.md in the docs.
--postgraphile Tweak generated SQL for PostGraphile usage.
https://www.graphile.org/postgraphile/
--postgraphile-password Password for the PostGraphile PostgreSQL user.
Default: $POSTGRAPHILE_PGPASSWORD, fallback random.
--postgrest Tweak generated SQL for PostgREST usage.
Please combine it with --schema.
https://postgrest.org/
--postgrest-password Password for the PostgREST PostgreSQL user \`web_anon\`.
Default: $POSTGREST_PGPASSWORD, fallback random.
--postgrest-query-cost-limit Define a cost limit [1] for queries executed by PostgREST
on behalf of a user. It is only enforced if
pg_plan_filter [2] is installed in the database!
Must be a positive float. Default: none
[1] https://www.postgresql.org/docs/14/using-explain.html
[2] https://github.com/pgexperts/pg_plan_filter
--import-metadata Create functions returning import metadata:
- gtfs_data_imported_at (timestamp with time zone)
- gtfs_via_postgres_version (text)
- gtfs_via_postgres_options (jsonb)
Examples:
gtfs-to-sql some-gtfs/*.txt | sponge | psql -b # import into PostgreSQL
gtfs-to-sql -u -- some-gtfs/*.txt | gzip >gtfs.sql.gz # generate a gzipped SQL dump
[1] https://developers.google.com/transit/gtfs/reference/extended-route-types
[2] https://groups.google.com/g/gtfs-changes/c/keT5rTPS7Y0/m/71uMz2l6ke0J
\n`)
process.exit(0)
}
if (flags.version) {
process.stdout.write(`${pkg.name} v${pkg.version}\n`)
process.exit(0)
}
const {basename, extname} = require('path')
const {pipeline} = require('stream')
const convertGtfsToSql = require('./index')
const DataError = require('./lib/data-error')
const files = args.map((file) => {
const name = basename(file, extname(file))
return {name, file}
})
const opt = {
silent: !!flags.silent,
requireDependencies: !!flags['require-dependencies'],
ignoreUnsupportedFiles: !!flags['ignore-unsupported'],
routeTypesScheme: flags['route-types-scheme'] || 'google-extended',
tripsWithoutShapeId: !!flags['trips-without-shape-id'],
routesWithoutAgencyId: !!flags['routes-without-agency-id'],
stopsLocationIndex: !!flags['stops-location-index'],
statsByRouteIdAndDate: flags['stats-by-route-date'] || 'none',
statsByAgencyIdAndRouteIdAndStopAndHour: flags['stats-by-agency-route-stop-hour'] || 'none',
statsActiveTripsByHour: flags['stats-active-trips-by-hour'] || 'none',
schema: flags['schema'] || 'public',
postgraphile: !!flags.postgraphile,
postgrest: !!flags.postgrest,
importMetadata: !!flags['import-metadata'],
}
if ('stops-without-level-id' in flags) {
opt.stopsWithoutLevelId = flags['stops-without-level-id']
}
if ('lower-case-lang-codes' in flags) {
opt.lowerCaseLanguageCodes = flags['lower-case-lang-codes']
}
if ('postgraphile-password' in flags) {
opt.postgraphilePassword = flags['postgraphile-password']
}
if ('postgrest-password' in flags) {
opt.postgrestPassword = flags['postgrest-password']
}
if ('postgrest-query-cost-limit' in flags) {
const limit = parseFloat(flags['postgrest-query-cost-limit'])
if (!Number.isFinite(limit) || limit < 0) {
console.error('Invalid --postgrest-query-cost-limit value.')
process.exit(1)
}
opt.lowerCaseLanguageCodes = limit
}
pipeline(
convertGtfsToSql(files, opt),
process.stdout,
(err) => {
if (!err) return;
if (err instanceof DataError) {
console.error(String(err))
} else if (err.code !== 'EPIPE') {
console.error(err)
}
process.exit(1)
}
)