timetable
Version:
Using GTFS and other sources to produce beautiful and usable timetables for public transport.
549 lines (471 loc) • 23.6 kB
text/coffeescript
"""
g/sttm^828812
g/trip^32565
g/route^811
%|g/route|name:foobar|^811
%|gtfs/stoptime|gtfs/trip^123|^556|gtfs/service^33421
%|gtfs/stoptime|gtfs/trip^123|^556
+ %|gtfs/trip|gtfs/route^8877|^123
------------------------------------------------------
= %|gtfs/stoptime|gtfs/route^8877
= %|gtfs/stoptime|gtfs/trip^123|^556|gtfs/route^8877
############################################################################################################
gtfs:
stoptime:
id: gtfs/stoptime/^876
stop-id: gtfs/stop/^123
trip-id: gtfs/trip/^456
...
stop:
id: gtfs/stop/^123
name: Bayerischer+Platz
...
trip:
id: gtfs/trip/^456
route-id: gtfs/route/^777
service-id: gtfs/service/^888
gtfs/stoptime/^876
gtfs/stoptime/^876/stop-id:gtfs/stop/^123
gtfs/stoptime/^876/trip-id:gtfs/trip/^456
gtfs/stop/^123
gtfs/stop/^123/name:Bayerischer+Platz
gtfs/trip/^456
############################################################################################################
gtfs:
stoptime:
^: gtfs/stoptime/^876
gtfs/stop/^: gtfs/stop/^123
gtfs/trip/^: gtfs/trip/^456
...
arr: 15:38
dep: 15:38
stop:
^: gtfs/stop/^123
name: Bayerischer+Platz
...
trip:
^: gtfs/trip/^456
gtfs/route/^: gtfs/route/^777
gtfs/service/^: gtfs/service/^888
route:
^: gtfs/route/^777
name: U4
# REALM/TYPE/^IDN
REALM/TYPE/^IDN|NAME:VALUE
REALM₀/TYPE₀/^IDN₀|>REALM₁/TYPE₁/^IDN₁
gtfs/route/^777
gtfs/route/^777|name:U4
gtfs/stop/^123
gtfs/stop/^123|=name:Bayerischer+Platz
gtfs/stoptime/^876
gtfs/stoptime/^876|=arr:15%2538
gtfs/stoptime/^876|=dep:15%2538
gtfs/stoptime/^876|>gtfs/stop/^123
gtfs/stoptime/^876|>gtfs/trip/^456
gtfs/trip/^456
gtfs/trip/^456|>gtfs/route/^777
gtfs/trip/^456|>gtfs/service/^888
gtfs/stoptime/^876|>gtfs/trip/^456
+ gtfs/trip/^456|>gtfs/route/^777
----------------------------------------------------------------
= gtfs/stoptime/^876|>>gtfs/route/^777
gtfs/stoptime/^876|>gtfs/trip/^456
gtfs/trip/^456|>gtfs/service/^888
----------------------------------------------------------------
= gtfs/stoptime/^876|>>gtfs/service/^888
============================================================================================================
%|REALM/TYPE|=NAME:VALUE|^IDN
%|REALM₀/TYPE₀|>N>=REALM₁/TYPE₁/^IDN₁|^IDN₀
%|gtfs/route|=name:U4|^777
%|gtfs/stoptime|=arr:15%2538|^876
%|gtfs/stoptime|=dep:15%2538|^876
%|gtfs/stoptime|>1>gtfs/stop/^123|^876
%|gtfs/stoptime|>1>gtfs/trip/^456|^876
%|gtfs/stoptime|>2>gtfs/route/^777|^876
%|gtfs/stoptime|>2>gtfs/service/^888|^876
%|gtfs/stop|=name:Bayerischer+Platz|^123
%|gtfs/trip|>gtfs/route/^777|^456
%|gtfs/trip|>gtfs/service/^888|^456
"""
############################################################################################################
# njs_util = require 'util'
# njs_path = require 'path'
njs_fs = require 'fs'
# njs_crypto = require 'crypto'
#...........................................................................................................
# BAP = require 'coffeenode-bitsnpieces'
TYPES = require 'coffeenode-types'
TEXT = require 'coffeenode-text'
TRM = require 'coffeenode-trm'
rpr = TRM.rpr.bind TRM
badge = 'TIMETABLE/GTFS-READER'
log = TRM.get_logger 'plain', badge
info = TRM.get_logger 'info', badge
whisper = TRM.get_logger 'whisper', badge
alert = TRM.get_logger 'alert', badge
debug = TRM.get_logger 'debug', badge
warn = TRM.get_logger 'warn', badge
help = TRM.get_logger 'help', badge
urge = TRM.get_logger 'urge', badge
echo = TRM.echo.bind TRM
rainbow = TRM.rainbow.bind TRM
#...........................................................................................................
options = require '../options'
global_data_limit = options[ 'data' ]?[ 'limit' ] ? Infinity
datasource_infos = ( require './get-datasource-infos' )()
REGISTRY = require './REGISTRY'
#...........................................................................................................
ASYNC = require 'async'
#...........................................................................................................
### https://github.com/loveencounterflow/pipedreams ###
P = require 'pipedreams'
$ = P.$.bind P
#...........................................................................................................
DEV = options[ 'mode' ] is 'dev'
############################################################################################################
# GENERIC METHODS
#-----------------------------------------------------------------------------------------------------------
@$set_id_from_gtfs_id = ->
return $ ( record, handler ) =>
gtfs_id = record[ 'gtfs-id' ]
gtfs_type = record[ 'gtfs-type' ]
return handler new Error "unable to register record without GTFS ID: #{rpr record}" unless gtfs_id?
return handler new Error "unable to register record without GTFS type: #{rpr record}" unless gtfs_type?
record[ 'id' ] = "gtfs/#{gtfs_type}/#{gtfs_id}"
handler null, record
#-----------------------------------------------------------------------------------------------------------
@$convert_latlon = ->
return $ ( record, handler ) =>
record[ 'lat' ] = parseFloat record[ 'lat' ]
record[ 'lon' ] = parseFloat record[ 'lon' ]
handler null, record
#-----------------------------------------------------------------------------------------------------------
@$_XXXX_add_id_indexes = ->
on_data = ( record ) ->
realm = 'gtfs'
type = record[ 'gtfs-type' ]
id = record[ 'gtfs-id' ]
#.......................................................................................................
for name, ref_id of record
match = name.match /^gtfs-(.+?)-id$/
continue unless match?
[ _, ref_type ] = match
ref_realm = 'gtfs'
index_record = 'id': "%|#{realm}/#{type}|#{ref_realm}/#{ref_type}/#{ref_id}|#{id}"
@emit 'data', index_record
# index_record = 'id': "%|#{ref_realm}/#{ref_type}/#{ref_id}<#{realm}/#{type}/#{id}"
# @emit 'data', index_record
#.......................................................................................................
@emit 'data', record
#.........................................................................................................
return P.through on_data, null
#-----------------------------------------------------------------------------------------------------------
@$index_on = ( names... ) ->
escape = @_escape_for_index
on_data = ( record ) ->
realm = 'gtfs'
type = record[ 'gtfs-type' ]
id = record[ 'gtfs-id' ]
#.......................................................................................................
for name in names
value = record[ name ]
continue unless value?
#.....................................................................................................
unless ( value_type = TYPES.type_of value ) is 'text'
throw new Error "building index from type #{rpr value_type} not currently supported"
#.....................................................................................................
value = escape value
index_record = 'id': "%|#{realm}/#{type}|#{name}:#{value}|#{id}"
@emit 'data', index_record
#.......................................................................................................
@emit 'data', record
#.........................................................................................................
return P.through on_data, null
#-----------------------------------------------------------------------------------------------------------
@_escape_for_index = ( text ) ->
R = text
R = R.replace /%/g, '%25'
R = R.replace /\|/g, '%7C'
R = R.replace /:/g, '%3A'
R = R.replace /\//g, '%2F'
return R
############################################################################################################
# SPECIFIC METHODS
#===========================================================================================================
# SPECIFIC METHODS: AGENCY
#-----------------------------------------------------------------------------------------------------------
@read_agency = ( registry, route, handler ) ->
input = P.create_readstream route, 'agency'
#.........................................................................................................
input.pipe P.$split()
.pipe P.$skip_empty()
# .pipe @$_XXX_save() # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# .pipe P.$sample 1 / 10, headers: true, seed: 5
.pipe P.$parse_csv()
.pipe @$clean_agency_record()
.pipe P.$delete_prefix 'agency_'
.pipe P.$set 'gtfs-type', 'agency'
.pipe P.$rename 'id', 'gtfs-id'
.pipe @$set_id_from_gtfs_id()
.pipe @$clean_agency_record()
.pipe P.$dasherize_field_names()
.pipe @$index_on 'name' # , 'url'
.pipe REGISTRY.$register registry
.pipe P.$collect_sample 4, ( _, sample ) -> whisper 'agency', sample
.pipe P.$on_end -> handler null
#.........................................................................................................
return null
#-----------------------------------------------------------------------------------------------------------
@$clean_agency_record = ->
return $ ( record, handler ) =>
delete record[ 'agency_phone' ]
delete record[ 'agency_lang' ]
handler null, record
#===========================================================================================================
# SPECIFIC METHODS: ROUTES
#-----------------------------------------------------------------------------------------------------------
### TAINT name clash (filesystem route vs. GTFS route) ###
@read_routes = ( registry, route, handler ) ->
ratio = if DEV then 1 / 10 else 1
input = P.create_readstream route, 'routes'
#.........................................................................................................
input.pipe P.$split()
.pipe P.$skip_empty()
.pipe P.$sample ratio, headers: true, seed: 5
.pipe P.$parse_csv()
.pipe @$clean_routes_record()
.pipe P.$dasherize_field_names()
.pipe P.$set 'gtfs-type', 'routes'
.pipe P.$rename 'route-id', 'gtfs-id'
.pipe P.$rename 'agency-id', 'gtfs-agency-id'
.pipe P.$rename 'route-short-name', 'name'
.pipe @$set_id_from_gtfs_id()
.pipe @$_XXXX_add_id_indexes()
.pipe @$index_on 'name'
.pipe REGISTRY.$register registry
.pipe P.$collect_sample 8, ( _, sample ) -> whisper 'route', sample
.pipe P.$on_end -> handler null
#.........................................................................................................
return null
#-----------------------------------------------------------------------------------------------------------
@$clean_routes_record = ->
return $ ( record, handler ) =>
# delete record[ 'route_id' ]
# delete record[ 'agency_id' ]
# delete record[ 'route_short_name' ]
delete record[ 'route_long_name' ]
delete record[ 'route_desc' ]
# delete record[ 'route_type' ]
delete record[ 'route_url' ]
delete record[ 'route_color' ]
delete record[ 'route_text_color' ]
handler null, record
#===========================================================================================================
# SPECIFIC METHODS: CALENDAR_DATES
#-----------------------------------------------------------------------------------------------------------
@read_calendar_dates = ( registry, route, handler ) ->
input = P.create_readstream route, 'calendar_dates'
#.........................................................................................................
input.pipe P.$split()
.pipe P.$skip_empty()
.pipe P.$parse_csv()
.pipe P.$sample 1 / 100, headers: true, seed: 5
.pipe @$clean_calendar_date_record()
.pipe P.$set 'gtfs-type', 'calendar_dates'
.pipe P.$rename 'service_id', 'gtfs-id'
.pipe @$set_id_from_gtfs_id()
.pipe REGISTRY.$register registry
.pipe P.$collect_sample 4, ( _, sample ) -> whisper 'service', sample
.pipe P.$on_end -> handler null
#.........................................................................................................
return null
#-----------------------------------------------------------------------------------------------------------
@$clean_calendar_date_record = ->
return $ ( record, handler ) =>
delete record[ 'exception_type' ]
handler null, record
#===========================================================================================================
# SPECIFIC METHODS: TRIPS
#-----------------------------------------------------------------------------------------------------------
@read_trips = ( registry, route, handler ) ->
input = P.create_readstream route, 'trips'
ratio = if DEV then 1 / 10 else 1
#.........................................................................................................
input.pipe P.$split()
.pipe P.$skip_empty()
.pipe P.$parse_csv()
.pipe P.$sample ratio, headers: true, seed: 5
.pipe @$clean_trip_record()
.pipe P.$delete_prefix 'trip_'
.pipe P.$dasherize_field_names()
.pipe P.$set 'gtfs-type', 'trips'
# .pipe P.$set 'gtfs-stoptimes', null
.pipe P.$rename 'id', 'gtfs-id'
.pipe P.$rename 'route-id', 'gtfs-routes-id'
.pipe P.$rename 'service-id', 'gtfs-service-id'
.pipe @$set_id_from_gtfs_id()
.pipe @$_XXXX_add_id_indexes()
.pipe REGISTRY.$register registry
.pipe P.$collect_sample 4, ( _, sample ) -> whisper 'trip', sample
.pipe P.$on_end -> handler null
#.........................................................................................................
return null
#-----------------------------------------------------------------------------------------------------------
@$clean_trip_record = ->
return $ ( record, handler ) =>
# delete record[ 'route_id' ]
# delete record[ 'service_id' ]
# delete record[ 'trip_id' ]
# delete record[ 'trip_headsign' ]
delete record[ 'trip_short_name' ]
delete record[ 'direction_id' ]
delete record[ 'block_id' ]
delete record[ 'shape_id' ]
handler null, record
#===========================================================================================================
# SPECIFIC METHODS: STOPTIMES
#-----------------------------------------------------------------------------------------------------------
@read_stop_times = ( registry, route, handler ) ->
input = P.create_readstream route, 'stop_times'
ratio = if DEV then 1 / 1000 else 1
#.........................................................................................................
input.pipe P.$split()
.pipe P.$skip_empty()
.pipe P.$sample ratio, headers: true
.pipe P.$parse_csv()
.pipe @$clean_stop_times_record()
.pipe P.$set 'gtfs-type', 'stop_times'
.pipe P.$dasherize_field_names()
.pipe P.$rename 'trip-id', 'gtfs-trips-id'
.pipe P.$rename 'stop-id', 'gtfs-stops-id'
.pipe P.$rename 'arrival-time', 'arr'
.pipe P.$rename 'departure-time', 'dep'
.pipe @$add_stoptimes_gtfs_id()
.pipe @$set_id_from_gtfs_id()
# .pipe @$add_stoptime_to_trip registry # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.pipe @$_XXXX_add_id_indexes()
# .pipe @$_XXXX_add_stop_times_index()
.pipe REGISTRY.$register registry
# .pipe @$register_stop_id registry
# .pipe @$_XXX_save() # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
.pipe P.$collect_sample 5, ( _, sample ) -> whisper 'stop_time', sample
.pipe P.$on_end -> handler null
#.........................................................................................................
return null
#-----------------------------------------------------------------------------------------------------------
@$clean_stop_times_record = ->
return $ ( record, handler ) =>
# delete record[ 'trip_id' ]
# delete record[ 'arrival_time' ]
# delete record[ 'departure_time' ]
# delete record[ 'stop_id' ]
# delete record[ 'stop_sequence' ]
delete record[ 'stop_headsign' ]
delete record[ 'pickup_type' ]
delete record[ 'drop_off_type' ]
delete record[ 'shape_dist_traveled' ]
handler null, record
#-----------------------------------------------------------------------------------------------------------
@$add_stoptimes_gtfs_id = ->
idx = 0
return $ ( record, handler ) =>
record[ 'gtfs-id' ] = "#{idx}"
idx += 1
handler null, record
#===========================================================================================================
# SPECIFIC METHODS: STOPS
#-----------------------------------------------------------------------------------------------------------
@read_stops = ( registry, route, handler ) ->
input = P.create_readstream route, 'stops'
#.........................................................................................................
input.pipe P.$split()
.pipe P.$skip_empty()
.pipe P.$parse_csv()
.pipe @$clean_stops_record()
.pipe P.$delete_prefix 'stop_'
.pipe P.$set 'gtfs-type', 'stops'
# .pipe P.$copy 'name', 'gtfs-name'
.pipe P.$rename 'id', 'gtfs-id'
.pipe @$set_id_from_gtfs_id()
.pipe @$convert_latlon()
.pipe @$index_on 'name'
.pipe REGISTRY.$register registry
.pipe P.$collect_sample 4, ( _, sample ) -> whisper 'stop', sample
.pipe P.$on_end -> handler null
#.........................................................................................................
return null
#-----------------------------------------------------------------------------------------------------------
@$clean_stops_record = ->
return $ ( record, handler ) =>
# delete record[ 'stop_id' ]
# delete record[ 'stop_code' ]
# delete record[ 'stop_name' ]
delete record[ 'stop_desc' ]
# delete record[ 'stop_lat' ]
# delete record[ 'stop_lon' ]
delete record[ 'zone_id' ]
delete record[ 'stop_url' ]
delete record[ 'location_type' ]
delete record[ 'parent_station' ]
handler null, record
#===========================================================================================================
# READ METHOD
#-----------------------------------------------------------------------------------------------------------
@main = ( registry, handler ) ->
t0 = 1 * new Date() # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#.........................................................................................................
for source_name, route_by_types of datasource_infos
tasks = []
no_source = []
no_method = []
ok_types = []
#.......................................................................................................
for gtfs_type in options[ 'data' ][ 'gtfs-types' ]
# if DEV
# if gtfs_type in [ 'agency', 'stops', ] # <<<<<<<<<<
# warn "skipping #{gtfs_type}" # <<<<<<<<<<
# continue # <<<<<<<<<<
route = route_by_types[ gtfs_type ]
unless route?
no_source.push "skipping #{source_name}/#{gtfs_type} (no source file)"
continue
help "found data source for #{source_name}/#{gtfs_type}"
#.....................................................................................................
method_name = "read_#{gtfs_type}"
method = @[ method_name ]
unless method?
no_method.push "no method to read GTFS data of type #{rpr gtfs_type}; skipping"
continue
method = method.bind @
ok_types.push gtfs_type
#.....................................................................................................
do ( method_name, method, route ) =>
tasks.push ( async_handler ) =>
help "#{badge}/#{method_name}"
method registry, route, ( P... ) =>
info "#{badge}/#{method_name}"
async_handler P...
#.......................................................................................................
for messages in [ no_source, no_method, ]
for message in messages
warn message
#.......................................................................................................
info "reading data for #{ok_types.length} type(s)"
info " (#{ok_types.join ', '})"
#.........................................................................................................
ASYNC.series tasks, ( error ) =>
throw error if error?
t1 = 1 * new Date() # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
urge 'dt:', ( t1 - t0 ) / 1000 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
handler null, registry
#.........................................................................................................
return null
############################################################################################################
# HELPERS
#===========================================================================================================
############################################################################################################
unless module.parent?
registry = REGISTRY.new_registry()
@main registry, ( error, registry ) ->
throw error if error?
info registry