UNPKG

timetable

Version:

Using GTFS and other sources to produce beautiful and usable timetables for public transport.

486 lines (439 loc) 22.7 kB
############################################################################################################ # 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/read-gtfs-data' 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 #........................................................................................................... T = require './TRANSFORMERS' as_transformer = T.as_transformer.bind T options = require '../options' global_data_limit = options[ 'data' ]?[ 'limit' ] ? Infinity datasource_infos = ( require './get-datasource-infos' )() create_readstream = require './create-readstream' REGISTRY = require './REGISTRY' #........................................................................................................... ASYNC = require 'async' #........................................................................................................... ### https://github.com/wdavidw/node-csv-parse ### _new_csv_parser = require 'csv-parse' new_csv_parser = -> _new_csv_parser options[ 'parser' ] ############################################################################################################ # GENERIC METHODS #----------------------------------------------------------------------------------------------------------- ### TAINT very Berlin-specific method, shouldnt appear here ### @_normalize_name = ( name ) -> name = name.replace /\s+\(Berlin\)(\s+Bus)?$/, '' name = name.replace /^(U|S\+U|S)\s+/, '' name = name.replace /^(Alexanderplatz) Bhf\/(.+)$/, '$1 ($2)' name = name.replace /^(Lichtenberg) Bhf\/(.+)$/, '$1 ($2)' name = name.replace /^(Alexanderplatz) Bhf/, '$1' name = name.replace /^(Zoologischer Garten) Bhf/, '$1' name = name.replace /^(Gesundbrunnen) Bhf/, '$1' name = name.replace /^(Potsdamer Platz) Bhf/, '$1' name = name.replace /^(Lichtenberg) Bhf/, '$1' name = name.replace /^(Friedrichstr\.) Bhf/, '$1' name = name.replace /^(Jungfernheide) Bhf/, '$1' name = name.replace /^(Stadtmitte) U[26]/, '$1' name = name.replace /^(.+)str\./, '$1straße' name = name.replace /^(.+)\s+Str\./, '$1 Straße' return name #----------------------------------------------------------------------------------------------------------- @_get_system_name = ( name ) -> name = name.toLowerCase() name = name.replace /,/g, '' name = name.replace /\./g, '' name = name.replace /\s+/g, '-' return name #----------------------------------------------------------------------------------------------------------- ### TAINT unify with following ### @$normalize_station_name = -> return as_transformer ( record, handler ) => record[ 'name' ] = @_normalize_name record[ 'name' ] handler null, record #----------------------------------------------------------------------------------------------------------- @$normalize_headsign = -> return as_transformer ( record, handler ) => record[ 'headsign' ] = @_normalize_name record[ 'headsign' ] handler null, record #----------------------------------------------------------------------------------------------------------- @$convert_latlon = -> return as_transformer ( record, handler ) => record[ 'lat' ] = parseFloat record[ 'lat' ] record[ 'lon' ] = parseFloat record[ 'lon' ] handler null, record #----------------------------------------------------------------------------------------------------------- @$register = ( registry ) -> return as_transformer ( record, handler ) => REGISTRY.register_gtfs registry, record return record ############################################################################################################ # SPECIFIC METHODS #=========================================================================================================== # SPECIFIC METHODS: AGENCIES #----------------------------------------------------------------------------------------------------------- @$clean_agency_record = -> return as_transformer ( record, handler ) => delete record[ 'agency_phone' ] delete record[ 'agency_lang' ] handler null, record #=========================================================================================================== # SPECIFIC METHODS: STOPTIMES #----------------------------------------------------------------------------------------------------------- @$clean_stoptime_record = -> return as_transformer ( 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_stoptime_idx = -> return as_transformer ( record, handler ) => record[ 'idx' ] = ( parseInt record[ 'stop-sequence' ], 10 ) - 1 delete record[ 'stop-sequence' ] handler null, record #=========================================================================================================== # SPECIFIC METHODS: ROUTES #----------------------------------------------------------------------------------------------------------- @$clean_route_record = -> return as_transformer ( 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: STATIONS #----------------------------------------------------------------------------------------------------------- @$clean_station_record = -> return as_transformer ( 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 #----------------------------------------------------------------------------------------------------------- @$add_station_system_name = -> return as_transformer ( record, handler ) => record[ '~name' ] = @_get_system_name record[ 'name' ] handler null, record #=========================================================================================================== # SPECIFIC METHODS: TRIPS #----------------------------------------------------------------------------------------------------------- @$clean_trip_record = -> return as_transformer ( 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 #----------------------------------------------------------------------------------------------------------- @$add_headsign_system_name = -> return as_transformer ( record, handler ) => record[ '~headsign' ] = @_get_system_name record[ 'headsign' ] handler null, record ############################################################################################################ # FINALIZATION #----------------------------------------------------------------------------------------------------------- #----------------------------------------------------------------------------------------------------------- @$add_agency_ids = ( registry ) -> return ( record ) -> record[ 'id' ] = record[ '%gtfs-id' ].replace /[-_]+$/, '' return record #----------------------------------------------------------------------------------------------------------- @$add_stoptime_ids = ( registry ) -> return ( record ) -> gtfs_stop_id = record[ '%gtfs-stop-id' ] gtfs_trip_id = record[ '%gtfs-trip-id' ] idx = record[ 'idx' ] record[ 'id' ] = "gtfs-stop:#{gtfs_stop_id}/gtfs-trip:#{gtfs_trip_id}/idx:#{idx}" return record #----------------------------------------------------------------------------------------------------------- @$add_route_ids = ( registry ) -> route_idx = -1 return ( record ) -> route_idx += 1 gtfs_agency_id = record[ '%gtfs-agency-id' ] gtfs_id = record[ '%gtfs-id' ] name = record[ 'name' ] agency = registry[ 'old' ][ gtfs_agency_id ] return handler new Error "unable to find agency with GTFS ID #{rpr gtfs_agency_id}" unless agency? agency_id = agency[ 'id' ] record[ 'id' ] = "route:#{route_idx}/#{agency_id}/name:#{name}" return record #----------------------------------------------------------------------------------------------------------- @$add_station_ids = ( registry ) -> return ( record ) -> stations_by_names = registry[ '%stations-by-names' ]?= {} sys_name = record[ '~name' ] stations = stations_by_names[ sys_name ]?= [] station_idx = stations.length stations.push record record[ 'id' ] = "station/name:#{sys_name}/idx:#{station_idx}" # whisper '©4p1', record[ 'id' ] return record #----------------------------------------------------------------------------------------------------------- @$add_trip_ids = ( registry ) -> return ( record ) -> gtfs_trip_id = record[ '%gtfs-id' ] gtfs_route_id = record[ '%gtfs-route-id' ] # route = registry[ 'old' ][ gtfs_route_id ] # sys_headsign = record[ '~headsign' ] # record[ 'id' ] = "station/headsign:#{sys_headsign}/gtfs-route-id:#{gtfs_route_id}/gtfs-trip-id:#{gtfs_trip_id}" ### does this make sense?? ### record[ 'id' ] = "gtfs-route-id:#{gtfs_route_id}/gtfs-trip-id:#{gtfs_trip_id}" # whisper record # whisper '©4p1', record[ 'id' ] return record #----------------------------------------------------------------------------------------------------------- @finalize = ( registry, handler ) -> method_by_types = 'agency': ( @$add_agency_ids registry ).bind @ 'stoptime': ( @$add_stoptime_ids registry ).bind @ 'route': ( @$add_route_ids registry ).bind @ 'station': ( @$add_station_ids registry ).bind @ 'trip': ( @$add_trip_ids registry ).bind @ for _, record of registry[ 'old' ] method = method_by_types[ label = record[ '~label' ] ] unless method? warn "unable to locate method `add_#{label}_ids; skipping" continue method record id = record[ 'id' ] unless id? warn "unable to find ID in #{record}; skipping" continue if ( duplicate = registry[ 'new' ][ id ] )? return handler new Error """ duplicate IDs: #{rpr duplicate} #{rpr record}""" registry[ 'new' ][ id ] = record handler null ############################################################################################################ # MAKE IT SO #----------------------------------------------------------------------------------------------------------- @read_agencies = ( route, registry, handler ) -> parser = new_csv_parser() input = create_readstream route, 'agencies' #......................................................................................................... input.on 'end', -> info 'ok: agencies' return handler null #......................................................................................................... input.pipe parser .pipe T.$as_pods() .pipe @$clean_agency_record() .pipe T.$delete_prefix 'agency_' .pipe T.$set '%gtfs-type', 'agency' .pipe T.$rename 'id', '%gtfs-id' .pipe T.$dasherize_field_names() .pipe @$register registry .pipe T.$show_sample input # .pipe T.$show_and_quit() #......................................................................................................... whisper 'reading GTFS agencies...' return null #----------------------------------------------------------------------------------------------------------- @read_stop_times = ( route, registry, handler ) -> parser = new_csv_parser() input = create_readstream route, 'stop_times' #......................................................................................................... input.on 'end', -> info 'ok: stoptimes' return handler null #......................................................................................................... input.pipe parser .pipe T.$skip global_data_limit .pipe T.$as_pods() .pipe @$clean_stoptime_record() # .pipe @$fix_ids() .pipe T.$delete_prefix 'trip_' .pipe T.$add_n4j_system_properties 'node', 'stoptime' .pipe T.$dasherize_field_names() .pipe T.$rename 'id', '%gtfs-trip-id' .pipe T.$rename 'stop-id', '%gtfs-stop-id' .pipe @$add_stoptime_idx() # .pipe T.$register registry .pipe T.$show_sample input # .pipe T.$show_and_quit() #......................................................................................................... whisper 'reading GTFS stoptimes...' return null #----------------------------------------------------------------------------------------------------------- ### TAINT name clash (filesystem route vs. GTFS route) ### @read_routes = ( route, registry, handler ) -> parser = new_csv_parser() input = create_readstream route, 'routes' #......................................................................................................... input.on 'end', -> info 'ok: routes' return handler null #......................................................................................................... input.pipe parser .pipe T.$skip global_data_limit .pipe T.$as_pods() .pipe @$clean_route_record() # .pipe @$fix_ids() .pipe T.$dasherize_field_names() .pipe T.$rename 'route-id', '%gtfs-id' .pipe T.$rename 'agency-id', '%gtfs-agency-id' .pipe T.$rename 'route-short-name', 'name' .pipe T.$add_n4j_system_properties 'node', 'route' # .pipe T.$register registry .pipe T.$show_sample input # .pipe T.$show_and_quit() #......................................................................................................... whisper 'reading GTFS routes...' return null #----------------------------------------------------------------------------------------------------------- @read_stops = ( route, registry, handler ) -> parser = new_csv_parser() input = create_readstream route, 'stops' #......................................................................................................... input.on 'end', -> info 'ok: stops' return handler null #......................................................................................................... input.pipe parser # .pipe T.$skip global_data_limit .pipe T.$as_pods() .pipe @$clean_station_record() .pipe T.$delete_prefix 'stop_' .pipe T.$set '%gtfs-type', 'stops' # .pipe T.$copy 'name', '%gtfs-name' # .pipe @$normalize_station_name() # .pipe @$add_station_system_name() .pipe T.$rename 'id', '%gtfs-id' # .pipe T.$add_n4j_system_properties 'node', 'station' .pipe @$convert_latlon() .pipe @$register registry .pipe T.$show_sample input # .pipe T.$show_and_quit() #......................................................................................................... whisper 'reading GTFS stops...' return null #----------------------------------------------------------------------------------------------------------- @read_trips = ( route, registry, handler ) -> parser = new_csv_parser() input = create_readstream route, 'trips' #......................................................................................................... input.on 'end', -> info 'ok: trips' return handler null #......................................................................................................... input.pipe parser .pipe T.$skip global_data_limit .pipe T.$as_pods() .pipe @$clean_trip_record() # .pipe @$fix_ids() .pipe T.$delete_prefix 'trip_' .pipe T.$dasherize_field_names() .pipe T.$rename 'id', '%gtfs-id' .pipe T.$rename 'route-id', '%gtfs-route-id' .pipe T.$rename 'service-id', '%gtfs-service-id' .pipe T.$copy 'headsign', '%gtfs-headsign' .pipe @$normalize_headsign() .pipe @$add_headsign_system_name() .pipe T.$add_n4j_system_properties 'node', 'trip' # .pipe T.$register registry .pipe T.$show_sample input # .pipe T.$show_and_quit() #......................................................................................................... whisper 'reading GTFS trips...' return null #=========================================================================================================== # READ METHOD #----------------------------------------------------------------------------------------------------------- @main = ( registry, handler ) -> #......................................................................................................... for source_name, route_by_types of datasource_infos tasks = [] no_source = [] no_method = [] ok_types = [] #....................................................................................................... for gtfs_type in options[ 'data' ][ 'gtfs-types' ] 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 = null switch gtfs_type when 'agency' then method = @read_agencies # when 'calendar_dates' then method = @read_calendar_dates # when 'calendar' then method = @read_calendar # when 'routes' then method = @read_routes # # when 'stop_times' then method = @read_stop_times when 'stops' then method = @read_stops # when 'transfers' then method = @read_transfers # when 'trips' then method = @read_trips 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, route ) => tasks.push ( async_handler ) => method route, registry, async_handler #....................................................................................................... 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 ', '})" #......................................................................................................... # limit = options[ 'stream-transform' ]?[ 'parallel' ] ? 1 ASYNC.series tasks, ( error ) => throw error if error? handler null, registry #......................................................................................................... return null ############################################################################################################ # HELPERS #=========================================================================================================== ############################################################################################################ unless module.parent? @main ( error, registry ) -> throw error if error? info registry