class FeedController < ApplicationController require 'csv' layout 'application', :except => :index def index channel = Channel.find(params[:channel_id]) api_key = ApiKey.find_by_api_key(get_userkey) @success = channel_permission?(channel, api_key) # set timezone correctly set_time_zone(params) # set limits limit = params[:results].to_i if params[:results] # check for access if @success # create options hash channel_options = { :only => channel_select_data(channel) } select_options = feed_select_data(channel) # get feed based on conditions feeds = Feed.find( :all, :conditions => { :channel_id => channel.id, :created_at => get_date_range(params) }, :select => select_options, :order => 'created_at desc', :limit => limit ) # keep track of whether data has been rounded already rounded = false # if a feed has data if !feeds.empty? # convert to timescales if necessary if timeparam_valid?(params[:timescale]) feeds = feeds_into_timescales(feeds) # convert to sums if necessary elsif timeparam_valid?(params[:sum]) feeds = feeds_into_sums(feeds) rounded = true # convert to averages if necessary elsif timeparam_valid?(params[:average]) feeds = feeds_into_averages(feeds) rounded = true # convert to medians if necessary elsif timeparam_valid?(params[:median]) feeds = feeds_into_medians(feeds) rounded = true end end # if a feed needs to be rounded if params[:round] and !rounded feeds = object_round(feeds, params[:round].to_i) end # set output correctly if params[:format] == 'xml' @channel_output = channel.to_xml(channel_options).sub('', '').strip @feed_output = feeds.to_xml(:skip_instruct => true).gsub(/\n/, "\n ").chop.chop elsif params[:format] == 'csv' @feed_output = feeds else @channel_output = channel.to_json(channel_options).chop @feed_output = feeds.to_json end # else no access, set error code else if params[:format] == 'xml' @channel_output = bad_channel_xml else @channel_output = '-1'.to_json end end # set callback for jsonp @callback = params[:callback] if params[:callback] # set csv headers if necessary @csv_headers = select_options if params[:format] == 'csv' # output proper http response if error render :text => '-1', :status => 400 and return if !@success # output data in proper format respond_to do |format| format.html format.json format.xml format.csv end end def show @channel = Channel.find(params[:channel_id]) @api_key = ApiKey.find_by_api_key(get_userkey) output = '-1' # get most recent entry if necessary params[:id] = @channel.last_entry_id if params[:id] == 'last' # set timezone correctly set_time_zone(params) @feed = Feed.find( :first, :conditions => { :channel_id => @channel.id, :entry_id => params[:id] }, :select => feed_select_data(@channel) ) @success = channel_permission?(@channel, @api_key) # if a feed needs to be rounded if params[:round] @feed = item_round(@feed, params[:round].to_i) end # check for access if @success # set output correctly if params[:format] == 'xml' output = @feed.to_xml elsif params[:format] == 'csv' @csv_headers = feed_select_data(@channel) elsif (params[:format] == 'txt' or params[:format] == 'text') output = add_prepend_append(@feed["field#{params[:field_id]}"]) else output = @feed.to_json end # else set error code else if params[:format] == 'xml' output = bad_feed_xml else output = '-1'.to_json end end # output data in proper format respond_to do |format| format.html { render :json => output } format.json { render :json => output, :callback => params[:callback] } format.xml { render :xml => output } format.csv format.text { render :text => output } end end private # only output these fields for channel def channel_select_data(channel) only = [:name, :created_at, :updated_at, :id, :last_entry_id] only += [:description] unless channel.description.blank? only += [:latitude] unless channel.latitude.blank? only += [:longitude] unless channel.longitude.blank? only += [:elevation] unless channel.elevation.blank? only += [:field1] unless channel.field1.blank? only += [:field2] unless channel.field2.blank? only += [:field3] unless channel.field3.blank? only += [:field4] unless channel.field4.blank? only += [:field5] unless channel.field5.blank? only += [:field6] unless channel.field6.blank? only += [:field7] unless channel.field7.blank? only += [:field8] unless channel.field8.blank? return only end # only output these fields for feed def feed_select_data(channel) only = [:created_at] only += [:entry_id] unless timeparam_valid?(params[:timescale]) or timeparam_valid?(params[:average]) or timeparam_valid?(params[:median]) or timeparam_valid?(params[:sum]) only += [:field1] unless channel.field1.blank? or (params[:field_id] and params[:field_id] != '1') only += [:field2] unless channel.field2.blank? or (params[:field_id] and params[:field_id] != '2') only += [:field3] unless channel.field3.blank? or (params[:field_id] and params[:field_id] != '3') only += [:field4] unless channel.field4.blank? or (params[:field_id] and params[:field_id] != '4') only += [:field5] unless channel.field5.blank? or (params[:field_id] and params[:field_id] != '5') only += [:field6] unless channel.field6.blank? or (params[:field_id] and params[:field_id] != '6') only += [:field7] unless channel.field7.blank? or (params[:field_id] and params[:field_id] != '7') only += [:field8] unless channel.field8.blank? or (params[:field_id] and params[:field_id] != '8') # add geolocation data if necessary if params[:location] and params[:location].upcase == 'TRUE' only += [:latitude] only += [:longitude] only += [:elevation] end # add status if necessary only += [:status] if params[:status] and params[:status].upcase == 'TRUE' return only end # checks for valid timescale def timeparam_valid?(timeparam) valid_minutes = [10, 15, 20, 30, 60, 240, 720, 1440] if timeparam and valid_minutes.include?(timeparam.to_i) return true else return false end end # applies rounding to an enumerable object def object_round(object, round=nil, match='field') object.each_with_index do |o, index| object[index] = item_round(o, round, match) end return object end # applies rounding to a single item's attributes if necessary def item_round(item, round=nil, match='field') # for each attribute item.attribute_names.each do |attr| # only add non-null numeric fields if attr.index(match) and !item[attr].nil? and is_a_number?(item[attr]) # keep track of whether the value contains commas comma_flag = (item[attr].to_s.index(',')) ? true : false # replace commas with decimals if appropriate item[attr] = item[attr].to_s.gsub(/,/, '.') if comma_flag # do the actual rounding item[attr] = sprintf "%.#{round}f", item[attr] # replace decimals with commas if appropriate item[attr] = item[attr].to_s.gsub(/\./, ',') if comma_flag end end # output new item return item end # slice feed into timescales def feeds_into_timescales(feeds) # convert timescale (minutes) into seconds seconds = params[:timescale].to_i * 60 # get floored time ranges start_time = get_floored_time(feeds.first.created_at, seconds) end_time = get_floored_time(feeds.last.created_at, seconds) # create empty array with appropriate size timeslices = Array.new((((end_time - start_time) / seconds).abs).floor) # create a blank clone of the first feed so that we only get the necessary attributes empty_feed = create_empty_clone(feeds.first) # add feeds to array feeds.each do |f| i = ((f.created_at - start_time) / seconds).floor f.created_at = start_time + i * seconds timeslices[i] = f if timeslices[i].nil? end # fill in empty array elements timeslices.each_index do |i| if timeslices[i].nil? current_feed = empty_feed.clone current_feed.created_at = (start_time + (i * seconds)) timeslices[i] = current_feed end end return timeslices end # slice feed into averages def feeds_into_averages(feeds) # convert timescale (minutes) into seconds seconds = params[:average].to_i * 60 # get floored time ranges start_time = get_floored_time(feeds.first.created_at, seconds) end_time = get_floored_time(feeds.last.created_at, seconds) # create empty array with appropriate size timeslices = Array.new(((end_time - start_time) / seconds).floor) # create a blank clone of the first feed so that we only get the necessary attributes empty_feed = create_empty_clone(feeds.first) # add feeds to array feeds.each do |f| i = ((f.created_at - start_time) / seconds).floor f.created_at = start_time + i * seconds # create multidimensional array timeslices[i] = [] if timeslices[i].nil? timeslices[i].push(f) end # keep track of whether numbers use commas as decimals comma_flag = false # fill in array timeslices.each_index do |i| # insert empty values if timeslices[i].nil? current_feed = empty_feed.clone current_feed.created_at = (start_time + (i * seconds)) timeslices[i] = current_feed # else average the inner array else sum_feed = empty_feed.clone sum_feed.created_at = timeslices[i].first.created_at # for each feed timeslices[i].each do |f| # for each attribute, add to sum_feed so that we have the total sum_feed.attribute_names.each do |attr| # only add non-null integer fields if attr.index('field') and !f[attr].nil? and is_a_number?(f[attr]) # set comma_flag once if we find a number with a comma comma_flag = true if !comma_flag and f[attr].to_s.index(',') # set initial data if sum_feed[attr].nil? sum_feed[attr] = parsefloat(f[attr]) # add data elsif f[attr] sum_feed[attr] = parsefloat(sum_feed[attr]) + parsefloat(f[attr]) end end end end # set to the averaged feed timeslices[i] = object_average(sum_feed, timeslices[i].length, comma_flag, params[:round]) end end return timeslices end # slice feed into medians def feeds_into_medians(feeds) # convert timescale (minutes) into seconds seconds = params[:median].to_i * 60 # get floored time ranges start_time = get_floored_time(feeds.first.created_at, seconds) end_time = get_floored_time(feeds.last.created_at, seconds) # create empty array with appropriate size timeslices = Array.new(((end_time - start_time) / seconds).floor) # create a blank clone of the first feed so that we only get the necessary attributes empty_feed = create_empty_clone(feeds.first) # add feeds to array feeds.each do |f| i = ((f.created_at - start_time) / seconds).floor f.created_at = start_time + i * seconds # create multidimensional array timeslices[i] = [] if timeslices[i].nil? timeslices[i].push(f) end # keep track of whether numbers use commas as decimals comma_flag = false # fill in array timeslices.each_index do |i| # insert empty values if timeslices[i].nil? current_feed = empty_feed.clone current_feed.created_at = (start_time + (i * seconds)) timeslices[i] = current_feed # else get median values for the inner array else # create blank hash called 'fields' to hold data fields = {} # for each feed timeslices[i].each do |f| # for each attribute f.attribute_names.each do |attr| if attr.index('field') # create blank array for each field fields["#{attr}"] = [] if fields["#{attr}"].nil? # push numeric field data onto its array if is_a_number?(f[attr]) # set comma_flag once if we find a number with a comma comma_flag = true if !comma_flag and f[attr].to_s.index(',') fields["#{attr}"].push(parsefloat(f[attr])) end end end end # sort fields arrays fields.each_key do |key| fields[key] = fields[key].compact.sort end # get the median median_feed = empty_feed.clone median_feed.created_at = timeslices[i].first.created_at median_feed.attribute_names.each do |attr| median_feed[attr] = object_median(fields[attr], comma_flag, params[:round]) if attr.index('field') end timeslices[i] = median_feed end end return timeslices end # slice feed into sums def feeds_into_sums(feeds) # convert timescale (minutes) into seconds seconds = params[:sum].to_i * 60 # get floored time ranges start_time = get_floored_time(feeds.first.created_at, seconds) end_time = get_floored_time(feeds.last.created_at, seconds) # create empty array with appropriate size timeslices = Array.new(((end_time - start_time) / seconds).floor) # create a blank clone of the first feed so that we only get the necessary attributes empty_feed = create_empty_clone(feeds.first) # add feeds to array feeds.each do |f| i = ((f.created_at - start_time) / seconds).floor f.created_at = start_time + i * seconds # create multidimensional array timeslices[i] = [] if timeslices[i].nil? timeslices[i].push(f) end # keep track of whether numbers use commas as decimals comma_flag = false # fill in array timeslices.each_index do |i| # insert empty values if timeslices[i].nil? current_feed = empty_feed.clone current_feed.created_at = (start_time + (i * seconds)) timeslices[i] = current_feed # else sum the inner array else sum_feed = empty_feed.clone sum_feed.created_at = timeslices[i].first.created_at # for each feed timeslices[i].each do |f| # for each attribute, add to sum_feed so that we have the total sum_feed.attribute_names.each do |attr| # only add non-null integer fields if attr.index('field') and !f[attr].nil? and is_a_number?(f[attr]) # set comma_flag once if we find a number with a comma comma_flag = true if !comma_flag and f[attr].to_s.index(',') # set initial data if sum_feed[attr].nil? sum_feed[attr] = parsefloat(f[attr]) # add data elsif f[attr] sum_feed[attr] = parsefloat(sum_feed[attr]) + parsefloat(f[attr]) end end end end # set to the summed feed timeslices[i] = object_sum(sum_feed, comma_flag, params[:round]) end end return timeslices end def is_a_number?(s) s.to_s.gsub(/,/, '.').match(/\A[+-]?\d+?(\.\d+)?\Z/) == nil ? false : true end def parsefloat(number) return number.to_s.gsub(/,/, '.').to_f end # gets the median for an object def object_median(object, comma_flag=false, round=nil) return nil if object.nil? length = object.length return nil if length == 0 output = '' # do the calculation if length % 2 == 0 output = (object[(length - 1) / 2] + object[length / 2]) / 2 else output = object[(length - 1) / 2] end output = sprintf "%.#{round}f", output if round and is_a_number?(output) # replace decimals with commas if appropriate output = output.to_s.gsub(/\./, ',') if comma_flag return output.to_s end # averages a summed object over length def object_average(object, length, comma_flag=false, round=nil) object.attribute_names.each do |attr| # only average non-null integer fields if !object[attr].nil? and is_a_number?(object[attr]) if round object[attr] = sprintf "%.#{round}f", (parsefloat(object[attr]) / length) else object[attr] = (parsefloat(object[attr]) / length).to_s end # replace decimals with commas if appropriate object[attr] = object[attr].gsub(/\./, ',') if comma_flag end end return object end # formats a summed object correctly def object_sum(object, comma_flag=false, round=nil) object.attribute_names.each do |attr| # only average non-null integer fields if !object[attr].nil? and is_a_number?(object[attr]) if round object[attr] = sprintf "%.#{round}f", parsefloat(object[attr]) else object[attr] = parsefloat(object[attr]).to_s end # replace decimals with commas if appropriate object[attr] = object[attr].gsub(/\./, ',') if comma_flag end end return object end # creates an empty clone of an object def create_empty_clone(object) empty_clone = object.dup empty_clone.attribute_names.each { |attr| empty_clone[attr] = nil } return empty_clone end # gets time floored to proper interval def get_floored_time(input_time, seconds) return Time.zone.at((input_time.to_f / seconds).floor * seconds) end end