app/apis/metro/realtime_updates.rb
require 'time'
module Metro
class RealtimeUpdates
def self.fetch(agency)
url = agency.gtfs_trip_updates_url
new(RealtimeProtobuf.fetch(url))
end
def initialize(feed)
@feed = feed
end
def for_stop_time(stop_time)
trip_update = for_trip(stop_time.trip)
if trip_update
return trip_update.stop_time_update_for(stop_time)
end
block_update = for_block(stop_time.trip)
if block_update
return block_update.stop_time_update_for(stop_time)
end
nil
end
def for_trip(trip)
metro_id = trip.remote_id.to_s
metro_trip = @feed[:entity].find { |entity| entity[:id] == metro_id }
if metro_trip
TripUpdate.new(metro_trip[:trip_update])
else
nil
end
end
def for_block(trip)
trip_ids = Trip.where(block_id: trip.block_id).pluck(:remote_id)
metro_trip = @feed[:entity].find { |entity| trip_ids.include?(entity[:id]) }
if metro_trip
BlockUpdate.new(trip, metro_trip[:trip_update])
else
nil
end
end
class TripUpdate
attr_reader :trip_id
def initialize(trip_update)
@trip_update = trip_update
@trip_id = @trip_update[:trip][:trip_id]
end
def stop_time_update_for(stop_time)
exact_match(stop_time) || nearest_match(stop_time)
end
protected
def stop_time_updates
@stop_time_updates ||= @trip_update[:stop_time_update].map { |stu| StopTimeUpdate.new(stu) }
end
def exact_match(stop_time)
stop_time_updates.find { |stu| stu.stop_id == stop_time.stop.remote_id }
end
def nearest_match(stop_time)
stop_time_updates
.select { |st| st.stop_sequence < stop_time.stop_sequence }
.sort_by { |st| st.stop_sequence }
.last
end
end
class BlockUpdate < TripUpdate
def initialize(trip, trip_update)
@trip_id = trip.id
@trip_update = trip_update
end
def stop_time_update_for(stop_time)
block_stop_time_updates(stop_time)
.sort_by { |st| st.stop_sequence }
.last
end
protected
def block_stop_time_updates(stop_time)
@stop_time_updates ||= @trip_update[:stop_time_update].map { |stu| BlockStopTimeUpdate.new(stop_time, stu) }
end
end
class StopTimeUpdate
def initialize(stop_time_update)
@stop_time_update = stop_time_update
end
def stop_id
@stop_time_update[:stop_id]
end
def stop_sequence
@stop_time_update[:stop_sequence]
end
# For whatever reason, arrivals can come after departures ¯\_(ツ)_/¯
def delay
[departure[:delay], arrival[:delay]].compact.max || 0
end
def departure_time
time = [arrival[:time], departure[:time]].compact.max
Time.at(time)
end
private
# arrival or departure might be nil, so default to an empty hash
def arrival
@stop_time_update[:arrival] || {}
end
def departure
@stop_time_update[:departure] || {}
end
end
class BlockStopTimeUpdate < StopTimeUpdate
def initialize(stop_time, stop_time_update)
@stop_time = stop_time
super(stop_time_update)
end
# We've used the delay from a Trip on the same block That means we can't
# use the departure time of that stop_time_update because it's going to
# be from the wrong trip. So calculate a departure time based on the
# scheduled departure time and the delay
def departure_time
@stop_time.departure_time + delay.seconds
end
end
end
end