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.
- checksums.yaml +7 -0
- data/.claude/commands/njtransit.md +196 -0
- data/.mcp.json.example +12 -0
- data/.mcp.json.sample +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +87 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +159 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +12 -0
- data/docs/plans/2025-01-24-njtransit-gem-design.md +112 -0
- data/docs/plans/2026-01-24-bus-api-design.md +119 -0
- data/docs/plans/2026-01-24-gtfs-implementation.md +2216 -0
- data/docs/plans/2026-01-24-gtfs-loader-design.md +351 -0
- data/docs/superpowers/plans/2026-03-26-dev-infra-and-agent.md +480 -0
- data/lefthook.yml +17 -0
- data/lib/njtransit/client.rb +291 -0
- data/lib/njtransit/configuration.rb +49 -0
- data/lib/njtransit/error.rb +50 -0
- data/lib/njtransit/gtfs/database.rb +145 -0
- data/lib/njtransit/gtfs/importer.rb +124 -0
- data/lib/njtransit/gtfs/models/route.rb +59 -0
- data/lib/njtransit/gtfs/models/stop.rb +63 -0
- data/lib/njtransit/gtfs/queries/routes_between.rb +62 -0
- data/lib/njtransit/gtfs/queries/schedule.rb +75 -0
- data/lib/njtransit/gtfs.rb +119 -0
- data/lib/njtransit/railtie.rb +9 -0
- data/lib/njtransit/resources/base.rb +35 -0
- data/lib/njtransit/resources/bus/enrichment.rb +105 -0
- data/lib/njtransit/resources/bus.rb +95 -0
- data/lib/njtransit/resources/bus_gtfs.rb +34 -0
- data/lib/njtransit/resources/rail.rb +47 -0
- data/lib/njtransit/resources/rail_gtfs.rb +27 -0
- data/lib/njtransit/tasks.rb +74 -0
- data/lib/njtransit/version.rb +5 -0
- data/lib/njtransit.rb +40 -0
- data/sig/njtransit.rbs +4 -0
- 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,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
|