njtransit 1.0.0

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/njtransit.md +196 -0
  3. data/.mcp.json.example +12 -0
  4. data/.mcp.json.sample +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +87 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +37 -0
  9. data/CLAUDE.md +159 -0
  10. data/CODE_OF_CONDUCT.md +84 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +148 -0
  13. data/Rakefile +12 -0
  14. data/docs/plans/2025-01-24-njtransit-gem-design.md +112 -0
  15. data/docs/plans/2026-01-24-bus-api-design.md +119 -0
  16. data/docs/plans/2026-01-24-gtfs-implementation.md +2216 -0
  17. data/docs/plans/2026-01-24-gtfs-loader-design.md +351 -0
  18. data/docs/superpowers/plans/2026-03-26-dev-infra-and-agent.md +480 -0
  19. data/lefthook.yml +17 -0
  20. data/lib/njtransit/client.rb +291 -0
  21. data/lib/njtransit/configuration.rb +49 -0
  22. data/lib/njtransit/error.rb +50 -0
  23. data/lib/njtransit/gtfs/database.rb +145 -0
  24. data/lib/njtransit/gtfs/importer.rb +124 -0
  25. data/lib/njtransit/gtfs/models/route.rb +59 -0
  26. data/lib/njtransit/gtfs/models/stop.rb +63 -0
  27. data/lib/njtransit/gtfs/queries/routes_between.rb +62 -0
  28. data/lib/njtransit/gtfs/queries/schedule.rb +75 -0
  29. data/lib/njtransit/gtfs.rb +119 -0
  30. data/lib/njtransit/railtie.rb +9 -0
  31. data/lib/njtransit/resources/base.rb +35 -0
  32. data/lib/njtransit/resources/bus/enrichment.rb +105 -0
  33. data/lib/njtransit/resources/bus.rb +95 -0
  34. data/lib/njtransit/resources/bus_gtfs.rb +34 -0
  35. data/lib/njtransit/resources/rail.rb +47 -0
  36. data/lib/njtransit/resources/rail_gtfs.rb +27 -0
  37. data/lib/njtransit/tasks.rb +74 -0
  38. data/lib/njtransit/version.rb +5 -0
  39. data/lib/njtransit.rb +40 -0
  40. data/sig/njtransit.rbs +4 -0
  41. metadata +177 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module GTFS
5
+ module Models
6
+ class Route
7
+ class << self
8
+ attr_accessor :db
9
+
10
+ def all
11
+ db[:routes].all.map { |row| new(row) }
12
+ end
13
+
14
+ def find(identifier)
15
+ row = db[:routes].where(route_id: identifier).first
16
+ row ||= db[:routes].where(route_short_name: identifier).first
17
+ row ? new(row) : nil
18
+ end
19
+
20
+ def where(conditions)
21
+ db[:routes].where(conditions).all.map { |row| new(row) }
22
+ end
23
+ end
24
+
25
+ attr_reader :route_id, :agency_id, :route_short_name, :route_long_name, :route_type, :route_color
26
+
27
+ def initialize(attributes)
28
+ @route_id = attributes[:route_id]
29
+ @agency_id = attributes[:agency_id]
30
+ @route_short_name = attributes[:route_short_name]
31
+ @route_long_name = attributes[:route_long_name]
32
+ @route_type = attributes[:route_type]
33
+ @route_color = attributes[:route_color]
34
+ end
35
+
36
+ def short_name
37
+ route_short_name
38
+ end
39
+
40
+ def long_name
41
+ route_long_name
42
+ end
43
+
44
+ def to_h
45
+ {
46
+ route_id: route_id,
47
+ agency_id: agency_id,
48
+ route_short_name: route_short_name,
49
+ route_long_name: route_long_name,
50
+ short_name: short_name,
51
+ long_name: long_name,
52
+ route_type: route_type,
53
+ route_color: route_color
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module GTFS
5
+ module Models
6
+ class Stop
7
+ class << self
8
+ attr_accessor :db
9
+
10
+ def all
11
+ db[:stops].all.map { |row| new(row) }
12
+ end
13
+
14
+ def find(stop_id)
15
+ row = db[:stops].where(stop_id: stop_id).first
16
+ row ? new(row) : nil
17
+ end
18
+
19
+ def find_by_code(stop_code)
20
+ row = db[:stops].where(stop_code: stop_code).first
21
+ row ? new(row) : nil
22
+ end
23
+
24
+ def where(conditions)
25
+ db[:stops].where(conditions).all.map { |row| new(row) }
26
+ end
27
+ end
28
+
29
+ attr_reader :stop_id, :stop_code, :stop_name, :stop_lat, :stop_lon, :zone_id
30
+
31
+ def initialize(attributes)
32
+ @stop_id = attributes[:stop_id]
33
+ @stop_code = attributes[:stop_code]
34
+ @stop_name = attributes[:stop_name]
35
+ @stop_lat = attributes[:stop_lat]
36
+ @stop_lon = attributes[:stop_lon]
37
+ @zone_id = attributes[:zone_id]
38
+ end
39
+
40
+ def lat
41
+ stop_lat
42
+ end
43
+
44
+ def lon
45
+ stop_lon
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ stop_id: stop_id,
51
+ stop_code: stop_code,
52
+ stop_name: stop_name,
53
+ stop_lat: stop_lat,
54
+ stop_lon: stop_lon,
55
+ lat: lat,
56
+ lon: lon,
57
+ zone_id: zone_id
58
+ }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module GTFS
5
+ module Queries
6
+ class RoutesBetween
7
+ attr_reader :db, :from, :to
8
+
9
+ def initialize(db, from:, to:)
10
+ @db = db
11
+ @from = from
12
+ @to = to
13
+ end
14
+
15
+ def call
16
+ from_stop_id = resolve_stop_id(from)
17
+ to_stop_id = resolve_stop_id(to)
18
+
19
+ return [] if from_stop_id.nil? || to_stop_id.nil?
20
+
21
+ route_ids = find_common_route_ids(from_stop_id, to_stop_id)
22
+ return [] if route_ids.empty?
23
+
24
+ route_short_names(route_ids)
25
+ end
26
+
27
+ private
28
+
29
+ def find_common_route_ids(from_stop_id, to_stop_id)
30
+ common_trips = find_common_trips(from_stop_id, to_stop_id)
31
+ return [] if common_trips.empty?
32
+
33
+ db[:trips].where(trip_id: common_trips).select_map(:route_id).uniq
34
+ end
35
+
36
+ def find_common_trips(from_stop_id, to_stop_id)
37
+ from_trips = trips_at_stop(from_stop_id)
38
+ to_trips = trips_at_stop(to_stop_id)
39
+ from_trips & to_trips
40
+ end
41
+
42
+ def trips_at_stop(stop_id)
43
+ db[:stop_times].where(stop_id: stop_id).select_map(:trip_id)
44
+ end
45
+
46
+ def route_short_names(route_ids)
47
+ db[:routes].where(route_id: route_ids).select_map(:route_short_name).uniq
48
+ end
49
+
50
+ def resolve_stop_id(identifier)
51
+ # Try as stop_id first
52
+ stop = db[:stops].where(stop_id: identifier).first
53
+ return identifier if stop
54
+
55
+ # Try as stop_code
56
+ stop = db[:stops].where(stop_code: identifier).first
57
+ stop&.dig(:stop_id)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module GTFS
5
+ module Queries
6
+ class Schedule
7
+ attr_reader :db, :route, :stop, :date
8
+
9
+ def initialize(db, route:, stop:, date:)
10
+ @db = db
11
+ @route = route
12
+ @stop = stop
13
+ @date = date
14
+ end
15
+
16
+ def call
17
+ route_id = resolve_route_id
18
+ stop_id = resolve_stop_id
19
+ service_ids = active_service_ids
20
+
21
+ return [] if route_id.nil? || stop_id.nil? || service_ids.empty?
22
+
23
+ trip_ids = find_trip_ids(route_id, service_ids)
24
+ return [] if trip_ids.empty?
25
+
26
+ fetch_stop_times(trip_ids, stop_id)
27
+ end
28
+
29
+ private
30
+
31
+ def find_trip_ids(route_id, service_ids)
32
+ db[:trips]
33
+ .where(route_id: route_id, service_id: service_ids)
34
+ .select_map(:trip_id)
35
+ end
36
+
37
+ def fetch_stop_times(trip_ids, stop_id)
38
+ db[:stop_times]
39
+ .where(trip_id: trip_ids, stop_id: stop_id)
40
+ .order(:arrival_time)
41
+ .all
42
+ .map { |row| format_stop_time(row) }
43
+ end
44
+
45
+ def format_stop_time(row)
46
+ {
47
+ trip_id: row[:trip_id],
48
+ arrival_time: row[:arrival_time],
49
+ departure_time: row[:departure_time],
50
+ stop_sequence: row[:stop_sequence]
51
+ }
52
+ end
53
+
54
+ def resolve_route_id
55
+ route_row = db[:routes].where(route_id: route).first
56
+ route_row ||= db[:routes].where(route_short_name: route).first
57
+ route_row&.dig(:route_id)
58
+ end
59
+
60
+ def resolve_stop_id
61
+ stop_row = db[:stops].where(stop_id: stop).first
62
+ stop_row ||= db[:stops].where(stop_code: stop).first
63
+ stop_row&.dig(:stop_id)
64
+ end
65
+
66
+ def active_service_ids
67
+ date_str = date.strftime("%Y%m%d")
68
+ db[:calendar_dates]
69
+ .where(date: date_str, exception_type: 1)
70
+ .select_map(:service_id)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gtfs/database"
4
+ require_relative "gtfs/importer"
5
+ require_relative "gtfs/models/stop"
6
+ require_relative "gtfs/models/route"
7
+ require_relative "gtfs/queries/routes_between"
8
+ require_relative "gtfs/queries/schedule"
9
+
10
+ module NJTransit
11
+ module GTFS
12
+ SEARCH_PATHS = [
13
+ "./bus_data",
14
+ "./vendor/bus_data",
15
+ "./docs/api/njtransit/bus_data"
16
+ ].freeze
17
+
18
+ class << self
19
+ def import(source_path, force: false)
20
+ importer = Importer.new(source_path, database_path)
21
+ validate_gtfs_directory!(importer, source_path)
22
+ importer.import(force: force)
23
+ end
24
+
25
+ def status
26
+ path = database_path
27
+ return { imported: false, path: path } unless Database.exists?(path)
28
+
29
+ build_status_hash(path)
30
+ end
31
+
32
+ def new
33
+ path = database_path
34
+
35
+ unless Database.exists?(path)
36
+ detected = detect_gtfs_path
37
+ raise GTFSNotImportedError.new(detected_path: detected)
38
+ end
39
+
40
+ QueryInterface.new(path)
41
+ end
42
+
43
+ def detect_gtfs_path
44
+ SEARCH_PATHS.find do |path|
45
+ File.directory?(path) && File.exist?(File.join(path, "agency.txt"))
46
+ end
47
+ end
48
+
49
+ def clear!
50
+ Database.connection(database_path)
51
+ Database.clear!
52
+ Database.disconnect
53
+ FileUtils.rm_f(database_path)
54
+ end
55
+
56
+ private
57
+
58
+ def database_path
59
+ NJTransit.configuration.gtfs_database_path
60
+ end
61
+
62
+ def validate_gtfs_directory!(importer, source_path)
63
+ return if importer.valid_gtfs_directory?
64
+
65
+ raise NJTransit::Error, "Invalid GTFS directory: #{source_path}. Must contain agency.txt, routes.txt, stops.txt"
66
+ end
67
+
68
+ def build_status_hash(path)
69
+ Database.connection(path)
70
+ db = Database.connection
71
+
72
+ { imported: true, path: path }.merge(table_counts(db)).merge(metadata_info(db))
73
+ end
74
+
75
+ def table_counts(db)
76
+ { routes: db[:routes].count, stops: db[:stops].count,
77
+ trips: db[:trips].count, stop_times: db[:stop_times].count }
78
+ end
79
+
80
+ def metadata_info(db)
81
+ metadata = db[:import_metadata].order(Sequel.desc(:id)).first
82
+ { imported_at: metadata&.dig(:imported_at), source_path: metadata&.dig(:source_path) }
83
+ end
84
+ end
85
+
86
+ class QueryInterface
87
+ attr_reader :db
88
+
89
+ def initialize(db_path)
90
+ Database.connection(db_path)
91
+ @db = Database.connection
92
+ setup_models
93
+ end
94
+
95
+ def stops
96
+ Models::Stop
97
+ end
98
+
99
+ def routes
100
+ Models::Route
101
+ end
102
+
103
+ def routes_between(from:, to:)
104
+ Queries::RoutesBetween.new(db, from: from, to: to).call
105
+ end
106
+
107
+ def schedule(route:, stop:, date:)
108
+ Queries::Schedule.new(db, route: route, stop: stop, date: date).call
109
+ end
110
+
111
+ private
112
+
113
+ def setup_models
114
+ Models::Stop.db = db
115
+ Models::Route.db = db
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "njtransit/tasks.rb"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ private
13
+
14
+ def get(path, params = {})
15
+ client.get(path, params)
16
+ end
17
+
18
+ def post(path, body = {})
19
+ client.post(path, body)
20
+ end
21
+
22
+ def put(path, body = {})
23
+ client.put(path, body)
24
+ end
25
+
26
+ def patch(path, body = {})
27
+ client.patch(path, body)
28
+ end
29
+
30
+ def delete(path, params = {})
31
+ client.delete(path, params)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module Resources
5
+ # GTFS enrichment for Bus API responses
6
+ module BusEnrichment
7
+ private
8
+
9
+ def ensure_gtfs_available!
10
+ gtfs
11
+ end
12
+
13
+ def gtfs
14
+ @gtfs ||= GTFS.new
15
+ end
16
+
17
+ def enrich_stops(stops)
18
+ return stops unless stops.is_a?(Array)
19
+
20
+ stops.each { |stop| enrich_stop_record(stop) }
21
+ end
22
+
23
+ def enrich_stop_record(stop)
24
+ stop_code = stop["stop_id"] || stop[:stop_id]
25
+ return unless stop_code
26
+
27
+ gtfs_stop = gtfs.stops.find_by_code(stop_code.to_s)
28
+ return unless gtfs_stop
29
+
30
+ stop["stop_lat"] = gtfs_stop.lat
31
+ stop["stop_lon"] = gtfs_stop.lon
32
+ stop["zone_id"] = gtfs_stop.zone_id
33
+ end
34
+
35
+ def enrich_stop_name(result, stop_number)
36
+ gtfs_stop = gtfs.stops.find_by_code(stop_number.to_s)
37
+ return result unless gtfs_stop
38
+
39
+ if result.is_a?(Hash)
40
+ result["stop_lat"] = gtfs_stop.lat
41
+ result["stop_lon"] = gtfs_stop.lon
42
+ end
43
+ result
44
+ end
45
+
46
+ def enrich_departures(departures)
47
+ return departures unless departures.is_a?(Array)
48
+
49
+ departures.each { |dep| enrich_departure_record(dep) }
50
+ end
51
+
52
+ def enrich_departure_record(dep)
53
+ enrich_departure_stop(dep)
54
+ enrich_departure_route(dep)
55
+ end
56
+
57
+ def enrich_departure_stop(dep)
58
+ stop_code = dep["stop_id"] || dep[:stop_id]
59
+ return unless stop_code
60
+
61
+ gtfs_stop = gtfs.stops.find_by_code(stop_code.to_s)
62
+ return unless gtfs_stop
63
+
64
+ dep["stop_lat"] = gtfs_stop.lat
65
+ dep["stop_lon"] = gtfs_stop.lon
66
+ end
67
+
68
+ def enrich_departure_route(dep)
69
+ route_name = dep["route"] || dep[:route]
70
+ return unless route_name
71
+
72
+ gtfs_route = gtfs.routes.find(route_name.to_s)
73
+ dep["route_long_name"] = gtfs_route.long_name if gtfs_route
74
+ end
75
+
76
+ def enrich_stops_nearby(stops)
77
+ return stops unless stops.is_a?(Array)
78
+
79
+ stops.each { |stop| enrich_stop_zone(stop) }
80
+ end
81
+
82
+ def enrich_stop_zone(stop)
83
+ stop_code = stop["stop_id"] || stop[:stop_id]
84
+ return unless stop_code
85
+
86
+ gtfs_stop = gtfs.stops.find_by_code(stop_code.to_s)
87
+ stop["zone_id"] = gtfs_stop.zone_id if gtfs_stop
88
+ end
89
+
90
+ def enrich_vehicles(vehicles)
91
+ return vehicles unless vehicles.is_a?(Array)
92
+
93
+ vehicles.each { |vehicle| enrich_vehicle_route(vehicle) }
94
+ end
95
+
96
+ def enrich_vehicle_route(vehicle)
97
+ route_name = vehicle["route"] || vehicle[:route]
98
+ return unless route_name
99
+
100
+ gtfs_route = gtfs.routes.find(route_name.to_s)
101
+ vehicle["route_long_name"] = gtfs_route.long_name if gtfs_route
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bus/enrichment"
4
+
5
+ module NJTransit
6
+ module Resources
7
+ class Bus < Base
8
+ include BusEnrichment
9
+
10
+ MODE = "BUS"
11
+ VALID_MODES = %w[BUS NLR HBLR RL ALL].freeze
12
+
13
+ def locations(mode: MODE)
14
+ post_form("/api/BUSDV2/getLocations", mode: validate_mode(mode))
15
+ end
16
+
17
+ def routes(mode: MODE)
18
+ post_form("/api/BUSDV2/getBusRoutes", mode: validate_mode(mode))
19
+ end
20
+
21
+ def directions(route:)
22
+ post_form("/api/BUSDV2/getBusDirectionsData", route: route)
23
+ end
24
+
25
+ def stops(route:, direction:, name_contains: nil, enrich: true)
26
+ ensure_gtfs_available! if enrich
27
+ params = { route: route, direction: direction }
28
+ params[:namecontains] = name_contains if name_contains
29
+ result = post_form("/api/BUSDV2/getStops", params)
30
+ enrich ? enrich_stops(result) : result
31
+ end
32
+
33
+ def stop_name(stop_number:, enrich: true)
34
+ ensure_gtfs_available! if enrich
35
+ result = post_form("/api/BUSDV2/getStopName", stopnum: stop_number)
36
+ enrich ? enrich_stop_name(result, stop_number) : result
37
+ end
38
+
39
+ def route_trips(location:, route:)
40
+ post_form("/api/BUSDV2/getRouteTrips", location: location, route: route)
41
+ end
42
+
43
+ def departures(stop:, route: nil, direction: nil, enrich: true)
44
+ ensure_gtfs_available! if enrich
45
+ params = { stop: stop }
46
+ params[:route] = route if route
47
+ params[:direction] = direction if direction
48
+ result = post_form("/api/BUSDV2/getBusDV", params)
49
+ enrich ? enrich_departures(result) : result
50
+ end
51
+
52
+ def trip_stops(internal_trip_number:, sched_dep_time:, timing_point_id: nil)
53
+ params = {
54
+ internal_trip_number: internal_trip_number,
55
+ sched_dep_time: sched_dep_time
56
+ }
57
+ params[:timing_point_id] = timing_point_id if timing_point_id
58
+ post_form("/api/BUSDV2/getTripStops", params)
59
+ end
60
+
61
+ def stops_nearby(lat:, lon:, radius:, mode: MODE, enrich: true, **options)
62
+ ensure_gtfs_available! if enrich
63
+ params = { lat: lat, lon: lon, radius: radius, mode: validate_mode(mode) }
64
+ params[:route] = options[:route] if options[:route]
65
+ params[:direction] = options[:direction] if options[:direction]
66
+ result = post_form("/api/BUSDV2/getBusLocationsData", params)
67
+ enrich ? enrich_stops_nearby(result) : result
68
+ end
69
+
70
+ def vehicles_nearby(lat:, lon:, radius:, mode: MODE, enrich: true)
71
+ ensure_gtfs_available! if enrich
72
+ result = post_form(
73
+ "/api/BUSDV2/getVehicleLocations",
74
+ lat: lat, lon: lon, radius: radius, mode: validate_mode(mode)
75
+ )
76
+ enrich ? enrich_vehicles(result) : result
77
+ end
78
+
79
+ private
80
+
81
+ def validate_mode(mode)
82
+ mode = mode.to_s.upcase
83
+ return mode if VALID_MODES.include?(mode)
84
+
85
+ raise ArgumentError,
86
+ "Invalid mode: #{mode}. Valid modes: #{VALID_MODES.join(", ")}"
87
+ end
88
+
89
+ def post_form(path, params = {})
90
+ params[:token] = client.token
91
+ client.post_form(path, params)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module Resources
5
+ class BusGTFS < Base
6
+ DEFAULT_PREFIX = "/api/GTFS"
7
+
8
+ def initialize(client, api_prefix: DEFAULT_PREFIX)
9
+ super(client)
10
+ @api_prefix = api_prefix
11
+ end
12
+
13
+ # Returns GTFS static schedule data as ZIP binary
14
+ def schedule_data
15
+ client.post_form_raw("#{@api_prefix}/getGTFS")
16
+ end
17
+
18
+ # Returns GTFS-RT alerts as protobuf binary
19
+ def alerts
20
+ client.post_form_raw("#{@api_prefix}/getAlerts")
21
+ end
22
+
23
+ # Returns GTFS-RT trip updates as protobuf binary
24
+ def trip_updates
25
+ client.post_form_raw("#{@api_prefix}/getTripUpdates")
26
+ end
27
+
28
+ # Returns GTFS-RT vehicle positions as protobuf binary
29
+ def vehicle_positions
30
+ client.post_form_raw("#{@api_prefix}/getVehiclePositions")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NJTransit
4
+ module Resources
5
+ class Rail < Base
6
+ def stations
7
+ post_form("/api/TrainData/getStationList")
8
+ end
9
+
10
+ def station_messages(station: nil, line: nil)
11
+ post_form("/api/TrainData/getStationMSG",
12
+ station: station || "", line: line || "")
13
+ end
14
+
15
+ def station_schedule(station:, njtonly: "1")
16
+ post_form("/api/TrainData/getStationSchedule",
17
+ station: station, NJT_Only: njtonly)
18
+ end
19
+
20
+ def train_schedule(station:, njtonly: "1")
21
+ post_form("/api/TrainData/getTrainSchedule",
22
+ station: station, NJT_Only: njtonly)
23
+ end
24
+
25
+ def train_schedule_19(station:, njtonly: "1")
26
+ post_form("/api/TrainData/getTrainSchedule19Rec",
27
+ station: station, NJT_Only: njtonly)
28
+ end
29
+
30
+ def train_stop_list(train_id:)
31
+ post_form("/api/TrainData/getTrainStopList",
32
+ trainID: train_id)
33
+ end
34
+
35
+ def vehicle_data
36
+ post_form("/api/TrainData/getVehicleData")
37
+ end
38
+
39
+ private
40
+
41
+ def post_form(path, params = {})
42
+ params[:token] = client.token
43
+ client.post_form(path, params)
44
+ end
45
+ end
46
+ end
47
+ end