joule 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.
- data/README.rdoc +18 -0
 - data/Rakefile +48 -0
 - data/lib/joule.rb +14 -0
 - data/lib/joule/array.rb +23 -0
 - data/lib/joule/calculator.rb +3 -0
 - data/lib/joule/calculator/marker_calculator.rb +40 -0
 - data/lib/joule/calculator/peak_power_calculator.rb +28 -0
 - data/lib/joule/calculator/power_calculator.rb +48 -0
 - data/lib/joule/csv.rb +1 -0
 - data/lib/joule/csv/parser.rb +68 -0
 - data/lib/joule/data_point.rb +20 -0
 - data/lib/joule/float.rb +39 -0
 - data/lib/joule/ibike.rb +2 -0
 - data/lib/joule/ibike/parser.rb +66 -0
 - data/lib/joule/ibike/properties.rb +52 -0
 - data/lib/joule/marker.rb +53 -0
 - data/lib/joule/powertap.rb +2 -0
 - data/lib/joule/powertap/parser.rb +82 -0
 - data/lib/joule/powertap/properties.rb +32 -0
 - data/lib/joule/srm.rb +2 -0
 - data/lib/joule/srm/parser.rb +152 -0
 - data/lib/joule/srm/properties.rb +31 -0
 - data/lib/joule/tcx.rb +2 -0
 - data/lib/joule/tcx/parser.rb +177 -0
 - data/lib/joule/tcx/properties.rb +11 -0
 - data/lib/joule/units_conversion.rb +55 -0
 - metadata +109 -0
 
    
        data/README.rdoc
    ADDED
    
    | 
         @@ -0,0 +1,18 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            = Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            == Description
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Joule is a  Ruby library for parsing bicycle powermeter data.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            == License
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            (The MIT License)
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            Copyright (c) 2008 - 2009 Andrew Olson
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     | 
    
        data/Rakefile
    ADDED
    
    | 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'rake'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'rake/testtask'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'rake/rdoctask'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            require 'rake/gempackagetask'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            task :default => [:test]
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            desc 'Test Joule.'
         
     | 
| 
      
 10 
     | 
    
         
            +
            Rake::TestTask.new(:test) do |t|
         
     | 
| 
      
 11 
     | 
    
         
            +
              t.libs << 'lib' 
         
     | 
| 
      
 12 
     | 
    
         
            +
              t.pattern = 'test/**/test_*.rb'
         
     | 
| 
      
 13 
     | 
    
         
            +
              t.verbose = true
         
     | 
| 
      
 14 
     | 
    
         
            +
            end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            spec = Gem::Specification.new do |s|
         
     | 
| 
      
 21 
     | 
    
         
            +
              s.name = "joule"
         
     | 
| 
      
 22 
     | 
    
         
            +
              s.summary = "A Ruby library for parsing bicycle powermeter data."
         
     | 
| 
      
 23 
     | 
    
         
            +
              s.description = ""
         
     | 
| 
      
 24 
     | 
    
         
            +
              s.homepage = "http://github.com/anolson/joule"
         
     | 
| 
      
 25 
     | 
    
         
            +
              
         
     | 
| 
      
 26 
     | 
    
         
            +
              s.version = "1.0.0"
         
     | 
| 
      
 27 
     | 
    
         
            +
              s.date = "2010-1-5"
         
     | 
| 
      
 28 
     | 
    
         
            +
              
         
     | 
| 
      
 29 
     | 
    
         
            +
              s.authors = ["Andrew Olson"]
         
     | 
| 
      
 30 
     | 
    
         
            +
              s.email = "anolson@gmail.com"
         
     | 
| 
      
 31 
     | 
    
         
            +
              
         
     | 
| 
      
 32 
     | 
    
         
            +
              s.require_paths = ["lib"]
         
     | 
| 
      
 33 
     | 
    
         
            +
              s.files = Dir["lib/**/*"] + ["README.rdoc", "Rakefile"]
         
     | 
| 
      
 34 
     | 
    
         
            +
              s.extra_rdoc_files = ["README.rdoc"]
         
     | 
| 
      
 35 
     | 
    
         
            +
              
         
     | 
| 
      
 36 
     | 
    
         
            +
              s.has_rdoc = true
         
     | 
| 
      
 37 
     | 
    
         
            +
              s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Joule", "--main", "README.rdoc"]
         
     | 
| 
      
 38 
     | 
    
         
            +
              
         
     | 
| 
      
 39 
     | 
    
         
            +
              s.rubygems_version = "1.3.4"
         
     | 
| 
      
 40 
     | 
    
         
            +
              s.required_rubygems_version = Gem::Requirement.new(">= 1.2")
         
     | 
| 
      
 41 
     | 
    
         
            +
              s.add_dependency("nokogiri", ">= 1.4.1")
         
     | 
| 
      
 42 
     | 
    
         
            +
              s.add_dependency("fastercsv", ">=1.4.0")
         
     | 
| 
      
 43 
     | 
    
         
            +
            end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            Rake::GemPackageTask.new(spec) do |pkg| 
         
     | 
| 
      
 47 
     | 
    
         
            +
              pkg.need_tar = true 
         
     | 
| 
      
 48 
     | 
    
         
            +
            end 
         
     | 
    
        data/lib/joule.rb
    ADDED
    
    | 
         @@ -0,0 +1,14 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'joule/array'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'joule/float'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            require 'joule/data_point'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'joule/marker'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'joule/calculator'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'joule/units_conversion'
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            require 'joule/csv'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require 'joule/ibike'
         
     | 
| 
      
 11 
     | 
    
         
            +
            require 'joule/powertap'
         
     | 
| 
      
 12 
     | 
    
         
            +
            require 'joule/srm'
         
     | 
| 
      
 13 
     | 
    
         
            +
            require 'joule/tcx'
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
    
        data/lib/joule/array.rb
    ADDED
    
    | 
         @@ -0,0 +1,23 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Array
         
     | 
| 
      
 2 
     | 
    
         
            +
              def summation
         
     | 
| 
      
 3 
     | 
    
         
            +
                inject{|sum, value| 
         
     | 
| 
      
 4 
     | 
    
         
            +
                  sum + value}
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
              def maximum
         
     | 
| 
      
 8 
     | 
    
         
            +
                inject {|max, value| value>max ? value : max}
         
     | 
| 
      
 9 
     | 
    
         
            +
              end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              def average
         
     | 
| 
      
 12 
     | 
    
         
            +
                summation/length
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              def average_maximum(size)
         
     | 
| 
      
 16 
     | 
    
         
            +
                mean_max = {:start => 0.0, :value=> 0.0}
         
     | 
| 
      
 17 
     | 
    
         
            +
                  each_index do |i|
         
     | 
| 
      
 18 
     | 
    
         
            +
                    mean = slice(i, size).average
         
     | 
| 
      
 19 
     | 
    
         
            +
                    mean > mean_max[:value] &&  mean_max = {:start => i, :value => mean}
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
                mean_max
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,40 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Calculator
         
     | 
| 
      
 3 
     | 
    
         
            +
                module MarkerCalculator
         
     | 
| 
      
 4 
     | 
    
         
            +
                  
         
     | 
| 
      
 5 
     | 
    
         
            +
                  def calculate_marker_averages(marker)
         
     | 
| 
      
 6 
     | 
    
         
            +
                    marker.average_power = Joule::Calculator::PowerCalculator::average(
         
     | 
| 
      
 7 
     | 
    
         
            +
                      self.data_points[marker.start..marker.end].collect() {|v| v.power})
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    marker.average_speed = Joule::Calculator::PowerCalculator::average(
         
     | 
| 
      
 10 
     | 
    
         
            +
                      self.data_points[marker.start..marker.end].collect() {|v| v.speed})
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    marker.average_cadence = Joule::Calculator::PowerCalculator::average(
         
     | 
| 
      
 13 
     | 
    
         
            +
                      self.data_points[marker.start..marker.end].collect() {|v| v.cadence})
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    marker.average_heartrate = Joule::Calculator::PowerCalculator::average(
         
     | 
| 
      
 16 
     | 
    
         
            +
                      self.data_points[marker.start..marker.end].collect() {|v| v.heartrate})
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def calculate_marker_maximums(marker)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    marker.maximum_power = Joule::Calculator::PowerCalculator::maximum(
         
     | 
| 
      
 21 
     | 
    
         
            +
                       self.data_points[marker.start..marker.end].collect() {|value| value.power})
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                     marker.maximum_speed = Joule::Calculator::PowerCalculator::maximum(
         
     | 
| 
      
 24 
     | 
    
         
            +
                       self.data_points[marker.start..marker.end].collect() {|value| value.speed})
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                     marker.maximum_cadence = Joule::Calculator::PowerCalculator::maximum(
         
     | 
| 
      
 27 
     | 
    
         
            +
                       self.data_points[marker.start..marker.end].collect() {|value| value.cadence})
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                     marker.maximum_heartrate = Joule::Calculator::PowerCalculator::maximum(
         
     | 
| 
      
 30 
     | 
    
         
            +
                       self.data_points[marker.start..marker.end].collect() {|value| value.heartrate})
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def calculate_marker_training_metrics(marker)
         
     | 
| 
      
 34 
     | 
    
         
            +
                    marker.normalized_power = Joule::Calculator::PowerCalculator::normalized_power( 
         
     | 
| 
      
 35 
     | 
    
         
            +
                      self.data_points[marker.start..marker.end].collect() {|value| value.power}, self.properties.record_interval)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                  
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,28 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Calculator
         
     | 
| 
      
 3 
     | 
    
         
            +
                module PeakPowerCalculator
         
     | 
| 
      
 4 
     | 
    
         
            +
                  
         
     | 
| 
      
 5 
     | 
    
         
            +
                  def calculate_peak_power_values(options = {})
         
     | 
| 
      
 6 
     | 
    
         
            +
                    power_values = @data_points.collect{|v| v.power}
         
     | 
| 
      
 7 
     | 
    
         
            +
                    options[:durations].each { |duration|
         
     | 
| 
      
 8 
     | 
    
         
            +
                      @peak_powers << calculate_peak_power_value(duration, options[:total_duration], power_values)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    }
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def calculate_peak_power_value(duration, total_duration, power_values)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    if duration > total_duration
         
     | 
| 
      
 15 
     | 
    
         
            +
                      { :duration => duration, 
         
     | 
| 
      
 16 
     | 
    
         
            +
                        :value => 0,
         
     | 
| 
      
 17 
     | 
    
         
            +
                        :start => 0 } 
         
     | 
| 
      
 18 
     | 
    
         
            +
                    else
         
     | 
| 
      
 19 
     | 
    
         
            +
                       peak_power = Joule::PowerCalculator::peak_power(power_values, (duration/@properties.record_interval))
         
     | 
| 
      
 20 
     | 
    
         
            +
                       { :duration => duration, 
         
     | 
| 
      
 21 
     | 
    
         
            +
                         :value => peak_power[:value],
         
     | 
| 
      
 22 
     | 
    
         
            +
                         :start => peak_power[:start] }
         
     | 
| 
      
 23 
     | 
    
         
            +
                    end
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Calculator
         
     | 
| 
      
 3 
     | 
    
         
            +
                class PowerCalculator
         
     | 
| 
      
 4 
     | 
    
         
            +
                  def self.average(values)
         
     | 
| 
      
 5 
     | 
    
         
            +
                    values.average
         
     | 
| 
      
 6 
     | 
    
         
            +
                  end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def self.maximum(values)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    values.maximum
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  def self.total(values)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    values.sum
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  def self.peak_power(values, size)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    values.average_maximum size
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
                
         
     | 
| 
      
 20 
     | 
    
         
            +
                  def self.training_stress_score(duration_seconds, threshold_power)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    if(threshold_power > 0)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      normalized_work = normalized_power * duration_seconds
         
     | 
| 
      
 23 
     | 
    
         
            +
                      raw_training_stress_score = normalized_work * intensity_factor(threshold_power)
         
     | 
| 
      
 24 
     | 
    
         
            +
                      (raw_training_stress_score/(threshold_power * 3600)) * 100
         
     | 
| 
      
 25 
     | 
    
         
            +
                    end
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  def self.intensity_factor(normalized_power, threshold_power)
         
     | 
| 
      
 29 
     | 
    
         
            +
                    if(threshold_power > 0)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      normalized_power/threshold_power
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
                
         
     | 
| 
      
 34 
     | 
    
         
            +
                  def self.normalized_power(values, record_interval)
         
     | 
| 
      
 35 
     | 
    
         
            +
                    thirty_second_record_count = 30 / record_interval
         
     | 
| 
      
 36 
     | 
    
         
            +
                    thirty_second_rolling_power = Array.new
         
     | 
| 
      
 37 
     | 
    
         
            +
                    if(values.length > thirty_second_record_count)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      values.slice(thirty_second_record_count..-1).each_slice(thirty_second_record_count) { |s|
         
     | 
| 
      
 39 
     | 
    
         
            +
                        thirty_second_rolling_power << s.average ** 4
         
     | 
| 
      
 40 
     | 
    
         
            +
                      }
         
     | 
| 
      
 41 
     | 
    
         
            +
                      thirty_second_rolling_power.average ** 0.25
         
     | 
| 
      
 42 
     | 
    
         
            +
                    else
         
     | 
| 
      
 43 
     | 
    
         
            +
                      0
         
     | 
| 
      
 44 
     | 
    
         
            +
                    end
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/joule/csv.rb
    ADDED
    
    | 
         @@ -0,0 +1 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'joule/csv/parser'
         
     | 
| 
         @@ -0,0 +1,68 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'fastercsv'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 4 
     | 
    
         
            +
              module CSV
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Parser
         
     | 
| 
      
 6 
     | 
    
         
            +
                  include Joule::Calculator::MarkerCalculator
         
     | 
| 
      
 7 
     | 
    
         
            +
                  include Joule::Calculator::PeakPowerCalculator
         
     | 
| 
      
 8 
     | 
    
         
            +
                  include Joule::UnitsConversion
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  attr_reader :properties, :markers, :data_points, :peak_powers
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def initialize(data)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @data = data
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @markers = Array.new
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @data_points = Array.new
         
     | 
| 
      
 17 
     | 
    
         
            +
                    @peak_powers = Array.new
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  def get_parser
         
     | 
| 
      
 21 
     | 
    
         
            +
                    header = FasterCSV.parse(@data).shift
         
     | 
| 
      
 22 
     | 
    
         
            +
                    if header[0].to_s.downcase.eql?("ibike")
         
     | 
| 
      
 23 
     | 
    
         
            +
                      return IbikeFileParser.new(data)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    else
         
     | 
| 
      
 25 
     | 
    
         
            +
                      return PowertapFileParser.new(data)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def parse(options = {})
         
     | 
| 
      
 30 
     | 
    
         
            +
                    parse_header
         
     | 
| 
      
 31 
     | 
    
         
            +
                    parse_markers
         
     | 
| 
      
 32 
     | 
    
         
            +
                    parse_data_points
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    if(options[:calculate_marker_values])
         
     | 
| 
      
 35 
     | 
    
         
            +
                      calculate_marker_values()
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    if(options[:calculate_peak_power_values])
         
     | 
| 
      
 39 
     | 
    
         
            +
                      calculate_peak_power_values(:durations => options[:durations], :total_duration => @markers.first.duration_seconds)
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  protected
         
     | 
| 
      
 45 
     | 
    
         
            +
                  def create_workout_marker(records)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    Marker.new(:start => 0, :end => records.size - 1, :comment => "")
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  def calculate_marker_values
         
     | 
| 
      
 50 
     | 
    
         
            +
                    @markers.each_with_index { |marker, i|
         
     | 
| 
      
 51 
     | 
    
         
            +
                      calculate_marker_averages marker      
         
     | 
| 
      
 52 
     | 
    
         
            +
                      calculate_marker_maximums marker
         
     | 
| 
      
 53 
     | 
    
         
            +
                      calculate_marker_training_metrics marker
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                      if i.eql?(0) 
         
     | 
| 
      
 56 
     | 
    
         
            +
                        marker.distance = @data_points.last.distance
         
     | 
| 
      
 57 
     | 
    
         
            +
                        marker.duration_seconds = @data_points.last.time
         
     | 
| 
      
 58 
     | 
    
         
            +
                      else
         
     | 
| 
      
 59 
     | 
    
         
            +
                        marker.distance = @data_points[marker.end].distance - @data_points[marker.start].distance
         
     | 
| 
      
 60 
     | 
    
         
            +
                        marker.duration_seconds = @data_points[marker.end].time - @data_points[marker.start].time
         
     | 
| 
      
 61 
     | 
    
         
            +
                      end
         
     | 
| 
      
 62 
     | 
    
         
            +
                      marker.energy = (marker.average_power.round * marker.duration_seconds)/1000        
         
     | 
| 
      
 63 
     | 
    
         
            +
                    }
         
     | 
| 
      
 64 
     | 
    
         
            +
                  end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,20 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              class DataPoint
         
     | 
| 
      
 3 
     | 
    
         
            +
                attr_accessor :altitude, :cadence, :distance, :heartrate, :latitude, :longitude, :power, :speed, :time, :time_of_day, :time_with_pauses, :torque 
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize()
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @time_of_day = 0
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @time = 0
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @time_with_pauses=0
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @power = 0.0
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @speed = 0.0
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @cadence = 0
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @distance = 0.0
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @altitude = 0.0
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @latitude = 0.0
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @longitude = 0.0
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @heartrate = 0
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @torque = 0.0
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
              end  
         
     | 
| 
      
 20 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/joule/float.rb
    ADDED
    
    | 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Copyright (c) 2004-2009 David Heinemeier Hansson
         
     | 
| 
      
 2 
     | 
    
         
            +
            # 
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Permission is hereby granted, free of charge, to any person obtaining
         
     | 
| 
      
 4 
     | 
    
         
            +
            # a copy of this software and associated documentation files (the
         
     | 
| 
      
 5 
     | 
    
         
            +
            # "Software"), to deal in the Software without restriction, including
         
     | 
| 
      
 6 
     | 
    
         
            +
            # without limitation the rights to use, copy, modify, merge, publish,
         
     | 
| 
      
 7 
     | 
    
         
            +
            # distribute, sublicense, and/or sell copies of the Software, and to
         
     | 
| 
      
 8 
     | 
    
         
            +
            # permit persons to whom the Software is furnished to do so, subject to
         
     | 
| 
      
 9 
     | 
    
         
            +
            # the following conditions:
         
     | 
| 
      
 10 
     | 
    
         
            +
            # 
         
     | 
| 
      
 11 
     | 
    
         
            +
            # The above copyright notice and this permission notice shall be
         
     | 
| 
      
 12 
     | 
    
         
            +
            # included in all copies or substantial portions of the Software.
         
     | 
| 
      
 13 
     | 
    
         
            +
            # 
         
     | 
| 
      
 14 
     | 
    
         
            +
            # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
         
     | 
| 
      
 15 
     | 
    
         
            +
            # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         
     | 
| 
      
 16 
     | 
    
         
            +
            # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         
     | 
| 
      
 17 
     | 
    
         
            +
            # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
         
     | 
| 
      
 18 
     | 
    
         
            +
            # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
         
     | 
| 
      
 19 
     | 
    
         
            +
            # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
         
     | 
| 
      
 20 
     | 
    
         
            +
            # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            class Float
         
     | 
| 
      
 23 
     | 
    
         
            +
              remove_method :round
         
     | 
| 
      
 24 
     | 
    
         
            +
             
         
     | 
| 
      
 25 
     | 
    
         
            +
              # Rounds the float with the specified precision.
         
     | 
| 
      
 26 
     | 
    
         
            +
              #
         
     | 
| 
      
 27 
     | 
    
         
            +
              # x = 1.337
         
     | 
| 
      
 28 
     | 
    
         
            +
              # x.round # => 1
         
     | 
| 
      
 29 
     | 
    
         
            +
              # x.round(1) # => 1.3
         
     | 
| 
      
 30 
     | 
    
         
            +
              # x.round(2) # => 1.34
         
     | 
| 
      
 31 
     | 
    
         
            +
              def round(precision = nil)
         
     | 
| 
      
 32 
     | 
    
         
            +
                if precision
         
     | 
| 
      
 33 
     | 
    
         
            +
                  magnitude = 10.0 ** precision
         
     | 
| 
      
 34 
     | 
    
         
            +
                  (self * magnitude).round / magnitude
         
     | 
| 
      
 35 
     | 
    
         
            +
                else
         
     | 
| 
      
 36 
     | 
    
         
            +
                  super()
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/joule/ibike.rb
    ADDED
    
    
| 
         @@ -0,0 +1,66 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module IBike
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Parser < Joule::CSV::Parser
         
     | 
| 
      
 4 
     | 
    
         
            +
                  IBIKE = '.csv'
         
     | 
| 
      
 5 
     | 
    
         
            +
                  SPEED = 0
         
     | 
| 
      
 6 
     | 
    
         
            +
                  WINDSPEED = 1
         
     | 
| 
      
 7 
     | 
    
         
            +
                  POWER = 2
         
     | 
| 
      
 8 
     | 
    
         
            +
                  DISTANCE = 3
         
     | 
| 
      
 9 
     | 
    
         
            +
                  CADENCE = 4
         
     | 
| 
      
 10 
     | 
    
         
            +
                  HEARTRATE = 5
         
     | 
| 
      
 11 
     | 
    
         
            +
                  ELEVATION = 6
         
     | 
| 
      
 12 
     | 
    
         
            +
                  SLOPE = 7
         
     | 
| 
      
 13 
     | 
    
         
            +
                  DFPM_POWER = 11
         
     | 
| 
      
 14 
     | 
    
         
            +
                  LATITUDE = 12
         
     | 
| 
      
 15 
     | 
    
         
            +
                  LONGITUDE = 13
         
     | 
| 
      
 16 
     | 
    
         
            +
                  TIMESTAMP = 14
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def parse_header()
         
     | 
| 
      
 19 
     | 
    
         
            +
                    records = FasterCSV.parse(@data)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    header = records.shift
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    @properties = Joule::IBike::Properties.new
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @properties.version=header[1]
         
     | 
| 
      
 24 
     | 
    
         
            +
                    @properties.units=header[2]
         
     | 
| 
      
 25 
     | 
    
         
            +
                    header = records.shift
         
     | 
| 
      
 26 
     | 
    
         
            +
                    @properties.date_time = Time.mktime(header[0].to_i, header[1].to_i, header[2].to_i, header[3].to_i, header[4].to_i, header[5].to_i)
         
     | 
| 
      
 27 
     | 
    
         
            +
                    records.shift
         
     | 
| 
      
 28 
     | 
    
         
            +
                    header = records.shift
         
     | 
| 
      
 29 
     | 
    
         
            +
                    @properties.total_weight = header[0]
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @properties.energy = header[1]
         
     | 
| 
      
 31 
     | 
    
         
            +
                    @properties.record_interval = header[4].to_i
         
     | 
| 
      
 32 
     | 
    
         
            +
                    @properties.starting_elevation = header[5]
         
     | 
| 
      
 33 
     | 
    
         
            +
                    @properties.total_climbing = header[6]
         
     | 
| 
      
 34 
     | 
    
         
            +
                    @properties.wheel_size = header[7]
         
     | 
| 
      
 35 
     | 
    
         
            +
                    @properties.temperature = header[8]
         
     | 
| 
      
 36 
     | 
    
         
            +
                    @properties.starting_pressure = header[9]
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @properties.wind_scaling = header[10]
         
     | 
| 
      
 38 
     | 
    
         
            +
                    @properties.riding_tilt = header[11]
         
     | 
| 
      
 39 
     | 
    
         
            +
                    @properties.calibration_weight = header[12]
         
     | 
| 
      
 40 
     | 
    
         
            +
                    @properties.cm = header[13]
         
     | 
| 
      
 41 
     | 
    
         
            +
                    @properties.cda = header[14]
         
     | 
| 
      
 42 
     | 
    
         
            +
                    @properties.crr = header[15]
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  def parse_data_points()
         
     | 
| 
      
 46 
     | 
    
         
            +
                    records = FasterCSV.parse(@data).slice(5..-1)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    records.each_with_index { |record, index|
         
     | 
| 
      
 48 
     | 
    
         
            +
                      data_point  = DataPoint.new
         
     | 
| 
      
 49 
     | 
    
         
            +
                      data_point.time  = index * @properties.record_interval
         
     | 
| 
      
 50 
     | 
    
         
            +
                      data_point.speed = convert_speed(record[SPEED].to_f)
         
     | 
| 
      
 51 
     | 
    
         
            +
                      data_point.power = record[POWER].to_f
         
     | 
| 
      
 52 
     | 
    
         
            +
                      data_point.distance = convert_distance(record[DISTANCE].to_f)
         
     | 
| 
      
 53 
     | 
    
         
            +
                      data_point.cadence = record[CADENCE].to_i
         
     | 
| 
      
 54 
     | 
    
         
            +
                      data_point.heartrate = record[HEARTRATE].to_i
         
     | 
| 
      
 55 
     | 
    
         
            +
                      @data_points << data_point 
         
     | 
| 
      
 56 
     | 
    
         
            +
                    }
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                  def parse_markers
         
     | 
| 
      
 60 
     | 
    
         
            +
                    records = FasterCSV.parse(@data).slice(5..-1)
         
     | 
| 
      
 61 
     | 
    
         
            +
                    @markers << create_workout_marker(records)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
              end
         
     | 
| 
      
 66 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,52 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module IBike
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Properties
         
     | 
| 
      
 4 
     | 
    
         
            +
                  ENGLISH_UNITS = "english"
         
     | 
| 
      
 5 
     | 
    
         
            +
                  METRIC_UNITS = "metric"
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                  attr_accessor :version 
         
     | 
| 
      
 8 
     | 
    
         
            +
                  attr_accessor :units 
         
     | 
| 
      
 9 
     | 
    
         
            +
                  attr_accessor :date_time 
         
     | 
| 
      
 10 
     | 
    
         
            +
                  attr_accessor :total_weight 
         
     | 
| 
      
 11 
     | 
    
         
            +
                  attr_accessor :energy 
         
     | 
| 
      
 12 
     | 
    
         
            +
                  attr_accessor :record_interval 
         
     | 
| 
      
 13 
     | 
    
         
            +
                  attr_accessor :starting_elevation 
         
     | 
| 
      
 14 
     | 
    
         
            +
                  attr_accessor :total_climbing 
         
     | 
| 
      
 15 
     | 
    
         
            +
                  attr_accessor :wheel_size 
         
     | 
| 
      
 16 
     | 
    
         
            +
                  attr_accessor :temperature 
         
     | 
| 
      
 17 
     | 
    
         
            +
                  attr_accessor :starting_pressure 
         
     | 
| 
      
 18 
     | 
    
         
            +
                  attr_accessor :wind_scaling 
         
     | 
| 
      
 19 
     | 
    
         
            +
                  attr_accessor :riding_tilt 
         
     | 
| 
      
 20 
     | 
    
         
            +
                  attr_accessor :calibration_weight 
         
     | 
| 
      
 21 
     | 
    
         
            +
                  attr_accessor :cm 
         
     | 
| 
      
 22 
     | 
    
         
            +
                  attr_accessor :cda 
         
     | 
| 
      
 23 
     | 
    
         
            +
                  attr_accessor :crr
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def distance_units_are_english?
         
     | 
| 
      
 26 
     | 
    
         
            +
                    self.units_are_english?
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def distance_units_are_metric?
         
     | 
| 
      
 30 
     | 
    
         
            +
                    self.units_are_metric?
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def speed_units_are_english?
         
     | 
| 
      
 34 
     | 
    
         
            +
                    self.units_are_english?
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def distance_units_are_metric?
         
     | 
| 
      
 38 
     | 
    
         
            +
                    self.units_are_metric?
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  def units_are_english?
         
     | 
| 
      
 42 
     | 
    
         
            +
                    self.units.eql?(ENGLISH_UNITS)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  def units_are_metric?
         
     | 
| 
      
 46 
     | 
    
         
            +
                    self.units.eql?(METRIC_UNITS)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
              end    
         
     | 
| 
      
 51 
     | 
    
         
            +
            end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
    
        data/lib/joule/marker.rb
    ADDED
    
    | 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Marker
         
     | 
| 
      
 2 
     | 
    
         
            +
              attr_accessor :active
         
     | 
| 
      
 3 
     | 
    
         
            +
              attr_accessor :average_cadence
         
     | 
| 
      
 4 
     | 
    
         
            +
              attr_accessor :average_heartrate
         
     | 
| 
      
 5 
     | 
    
         
            +
              attr_accessor :average_power
         
     | 
| 
      
 6 
     | 
    
         
            +
              attr_accessor :average_power_to_weight
         
     | 
| 
      
 7 
     | 
    
         
            +
              attr_accessor :average_speed
         
     | 
| 
      
 8 
     | 
    
         
            +
              attr_accessor :comment
         
     | 
| 
      
 9 
     | 
    
         
            +
              attr_accessor :duration_seconds
         
     | 
| 
      
 10 
     | 
    
         
            +
              attr_accessor :distance
         
     | 
| 
      
 11 
     | 
    
         
            +
              attr_accessor :end
         
     | 
| 
      
 12 
     | 
    
         
            +
              attr_accessor :energy
         
     | 
| 
      
 13 
     | 
    
         
            +
              attr_accessor :intensity_factor
         
     | 
| 
      
 14 
     | 
    
         
            +
              attr_accessor :maximum_cadence
         
     | 
| 
      
 15 
     | 
    
         
            +
              attr_accessor :maximum_heartrate
         
     | 
| 
      
 16 
     | 
    
         
            +
              attr_accessor :maximum_power
         
     | 
| 
      
 17 
     | 
    
         
            +
              attr_accessor :maximum_power_to_weight
         
     | 
| 
      
 18 
     | 
    
         
            +
              attr_accessor :maximum_speed 
         
     | 
| 
      
 19 
     | 
    
         
            +
              attr_accessor :normalized_power 
         
     | 
| 
      
 20 
     | 
    
         
            +
              attr_accessor :start
         
     | 
| 
      
 21 
     | 
    
         
            +
              attr_accessor :start_time
         
     | 
| 
      
 22 
     | 
    
         
            +
              attr_accessor :training_stress_score 
         
     | 
| 
      
 23 
     | 
    
         
            +
              
         
     | 
| 
      
 24 
     | 
    
         
            +
              
         
     | 
| 
      
 25 
     | 
    
         
            +
              def initialize(options = {})
         
     | 
| 
      
 26 
     | 
    
         
            +
                @active = true
         
     | 
| 
      
 27 
     | 
    
         
            +
                @average_cadence = 0
         
     | 
| 
      
 28 
     | 
    
         
            +
                @average_heartrate = 0
         
     | 
| 
      
 29 
     | 
    
         
            +
                @average_power = 0.0
         
     | 
| 
      
 30 
     | 
    
         
            +
                @average_power_to_weight = 0.0
         
     | 
| 
      
 31 
     | 
    
         
            +
                @average_speed = 0.0
         
     | 
| 
      
 32 
     | 
    
         
            +
                @comment = ""
         
     | 
| 
      
 33 
     | 
    
         
            +
                @duration_seconds = 0
         
     | 
| 
      
 34 
     | 
    
         
            +
                @distance = 0.0
         
     | 
| 
      
 35 
     | 
    
         
            +
                @end = options[:end]
         
     | 
| 
      
 36 
     | 
    
         
            +
                @energy = 0
         
     | 
| 
      
 37 
     | 
    
         
            +
                @intensity_factor = 0
         
     | 
| 
      
 38 
     | 
    
         
            +
                @maximum_cadence = 0
         
     | 
| 
      
 39 
     | 
    
         
            +
                @maximum_heartrate = 0
         
     | 
| 
      
 40 
     | 
    
         
            +
                @maximum_power = 0.0
         
     | 
| 
      
 41 
     | 
    
         
            +
                @maximum_power_to_weight = 0.0
         
     | 
| 
      
 42 
     | 
    
         
            +
                @maximum_speed = 0.0
         
     | 
| 
      
 43 
     | 
    
         
            +
                @normalized_power = 0
         
     | 
| 
      
 44 
     | 
    
         
            +
                @start = options[:start]
         
     | 
| 
      
 45 
     | 
    
         
            +
                @training_stress_score = 0.0
         
     | 
| 
      
 46 
     | 
    
         
            +
                
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
              
         
     | 
| 
      
 49 
     | 
    
         
            +
              def start_time_in_seconds
         
     | 
| 
      
 50 
     | 
    
         
            +
                (@start_time.hour * 3600) + (@start_time.min * 60) + @start_time.sec
         
     | 
| 
      
 51 
     | 
    
         
            +
              end
         
     | 
| 
      
 52 
     | 
    
         
            +
            end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,82 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module PowerTap
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Parser < Joule::CSV::Parser
         
     | 
| 
      
 4 
     | 
    
         
            +
                  MINUTES = 0
         
     | 
| 
      
 5 
     | 
    
         
            +
                  TORQUE = 1
         
     | 
| 
      
 6 
     | 
    
         
            +
                  SPEED = 2
         
     | 
| 
      
 7 
     | 
    
         
            +
                  POWER = 3
         
     | 
| 
      
 8 
     | 
    
         
            +
                  DISTANCE = 4
         
     | 
| 
      
 9 
     | 
    
         
            +
                  CADENCE = 5
         
     | 
| 
      
 10 
     | 
    
         
            +
                  HEARTRATE = 6
         
     | 
| 
      
 11 
     | 
    
         
            +
                  MARKER = 7
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def parse_header()
         
     | 
| 
      
 14 
     | 
    
         
            +
                    header = FasterCSV.parse(@data).shift
         
     | 
| 
      
 15 
     | 
    
         
            +
                    records = FasterCSV.parse(@data) 
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @properties = Joule::PowerTap::Properties.new
         
     | 
| 
      
 17 
     | 
    
         
            +
                    @properties.speed_units = header[SPEED].to_s.downcase
         
     | 
| 
      
 18 
     | 
    
         
            +
                    @properties.power_units = header[POWER].to_s.downcase
         
     | 
| 
      
 19 
     | 
    
         
            +
                    @properties.distance_units = header[DISTANCE].to_s.downcase
         
     | 
| 
      
 20 
     | 
    
         
            +
                    calculate_record_interval(records)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  def parse_data_points()
         
     | 
| 
      
 24 
     | 
    
         
            +
                    records = FasterCSV.parse(@data) 
         
     | 
| 
      
 25 
     | 
    
         
            +
                    records.shift
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    records.each { |record|
         
     | 
| 
      
 28 
     | 
    
         
            +
                      data_point = DataPoint.new
         
     | 
| 
      
 29 
     | 
    
         
            +
                      data_point.time  = (record[MINUTES].to_f * 60).to_i
         
     | 
| 
      
 30 
     | 
    
         
            +
                      data_point.torque = record[TORQUE].to_f
         
     | 
| 
      
 31 
     | 
    
         
            +
                      data_point.speed = convert_speed(record[SPEED].to_f)
         
     | 
| 
      
 32 
     | 
    
         
            +
                      data_point.power = record[POWER].to_f
         
     | 
| 
      
 33 
     | 
    
         
            +
                      data_point.distance = convert_distance(record[DISTANCE].to_f)
         
     | 
| 
      
 34 
     | 
    
         
            +
                      data_point.cadence = record[CADENCE].to_i
         
     | 
| 
      
 35 
     | 
    
         
            +
                      data_point.heartrate = (record[HEARTRATE].to_i < 0) && 0 || record[HEARTRATE].to_i
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                      @data_points << data_point
         
     | 
| 
      
 38 
     | 
    
         
            +
                    }  
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  def parse_markers
         
     | 
| 
      
 42 
     | 
    
         
            +
                    records = FasterCSV.parse(@data) 
         
     | 
| 
      
 43 
     | 
    
         
            +
                    records.shift
         
     | 
| 
      
 44 
     | 
    
         
            +
                    @markers << create_workout_marker(records)
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                    current_marker_index = 0
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    records.each_with_index { |record, index|
         
     | 
| 
      
 49 
     | 
    
         
            +
                      if(record[MARKER].to_i > current_marker_index )
         
     | 
| 
      
 50 
     | 
    
         
            +
                        create_marker(index)
         
     | 
| 
      
 51 
     | 
    
         
            +
                        current_marker_index = current_marker_index + 1
         
     | 
| 
      
 52 
     | 
    
         
            +
                      end 
         
     | 
| 
      
 53 
     | 
    
         
            +
                    }
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    #set the end of the last marker
         
     | 
| 
      
 56 
     | 
    
         
            +
                    set_previous_marker_end(records.size - 1)  
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                  def create_marker(start)
         
     | 
| 
      
 60 
     | 
    
         
            +
                    if(@markers.size.eql?(1))
         
     | 
| 
      
 61 
     | 
    
         
            +
                      @markers << Marker.new(:start => 0)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end  
         
     | 
| 
      
 63 
     | 
    
         
            +
                    set_previous_marker_end(start - 1)
         
     | 
| 
      
 64 
     | 
    
         
            +
                    @markers << Marker.new(:start => start)
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  def set_previous_marker_end(value)
         
     | 
| 
      
 68 
     | 
    
         
            +
                    if(@markers.size > 1)
         
     | 
| 
      
 69 
     | 
    
         
            +
                      @markers.last.end = value
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
      
 71 
     | 
    
         
            +
                  end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                  def calculate_record_interval(records)
         
     | 
| 
      
 74 
     | 
    
         
            +
                    times = Array.new
         
     | 
| 
      
 75 
     | 
    
         
            +
                    records[1..30].each_slice(2) {|s| times << ((s[1][MINUTES].to_f - s[0][MINUTES].to_f)  * 60) }
         
     | 
| 
      
 76 
     | 
    
         
            +
                    @properties.record_interval = times.average.round
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
                
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,32 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module PowerTap
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Properties
         
     | 
| 
      
 4 
     | 
    
         
            +
                  ENGLISH_SPEED_UNITS = "miles/h"
         
     | 
| 
      
 5 
     | 
    
         
            +
                  ENGLISH_POWER_UNITS = "watts"
         
     | 
| 
      
 6 
     | 
    
         
            +
                  ENGLISH_DISTANCE_UNITS = "miles"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  METRIC_SPEED_UNITS = "km/h"
         
     | 
| 
      
 9 
     | 
    
         
            +
                  METRIC_POWER_UNITS = "watts"
         
     | 
| 
      
 10 
     | 
    
         
            +
                  METRIC_DISTANCE_UNITS = "km"
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  attr_accessor :speed_units, :power_units, :distance_units, :record_interval
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  def speed_units_are_english?()
         
     | 
| 
      
 15 
     | 
    
         
            +
                    return self.speed_units.eql?(ENGLISH_SPEED_UNITS)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def speed_units_are_metric?()
         
     | 
| 
      
 19 
     | 
    
         
            +
                    return self.speed_units.eql?(METRIC_SPEED_UNITS)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  def distance_units_are_english?()
         
     | 
| 
      
 23 
     | 
    
         
            +
                    return self.distance_units.eql?(ENGLISH_DISTANCE_UNITS)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  def distance_units_are_metric?()
         
     | 
| 
      
 27 
     | 
    
         
            +
                    return self.distance_units.eql?(METRIC_DISTANCE_UNITS)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end 
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/joule/srm.rb
    ADDED
    
    
| 
         @@ -0,0 +1,152 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'kconv'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 4 
     | 
    
         
            +
              module SRM
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Parser
         
     | 
| 
      
 6 
     | 
    
         
            +
                   include Joule::Calculator::MarkerCalculator
         
     | 
| 
      
 7 
     | 
    
         
            +
                   include Joule::Calculator::PeakPowerCalculator
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                   SRM = '.srm'
         
     | 
| 
      
 10 
     | 
    
         
            +
                   HEADER_SIZE=86
         
     | 
| 
      
 11 
     | 
    
         
            +
                   MARKER_SIZE=270
         
     | 
| 
      
 12 
     | 
    
         
            +
                   BLOCK_SIZE=6
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                   attr_reader :properties, :markers, :data_points, :peak_powers
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                   def initialize(data)
         
     | 
| 
      
 17 
     | 
    
         
            +
                     @data = data  
         
     | 
| 
      
 18 
     | 
    
         
            +
                     @data_points = Array.new
         
     | 
| 
      
 19 
     | 
    
         
            +
                     @markers = Array.new
         
     | 
| 
      
 20 
     | 
    
         
            +
                     @peak_powers = Array.new
         
     | 
| 
      
 21 
     | 
    
         
            +
                   end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                   def parse(options = {})
         
     | 
| 
      
 24 
     | 
    
         
            +
                     parse_header
         
     | 
| 
      
 25 
     | 
    
         
            +
                     parse_markers
         
     | 
| 
      
 26 
     | 
    
         
            +
                     parse_blocks
         
     | 
| 
      
 27 
     | 
    
         
            +
                     parse_data_points
         
     | 
| 
      
 28 
     | 
    
         
            +
                     parse_data_point_times
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                     if(options[:calculate_marker_values])
         
     | 
| 
      
 31 
     | 
    
         
            +
                       calculate_marker_values()
         
     | 
| 
      
 32 
     | 
    
         
            +
                     end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                     if(options[:calculate_peak_power_values])
         
     | 
| 
      
 35 
     | 
    
         
            +
                       calculate_peak_power_values(:durations => options[:durations], :total_duration => @markers.first.duration_seconds)
         
     | 
| 
      
 36 
     | 
    
         
            +
                     end
         
     | 
| 
      
 37 
     | 
    
         
            +
                   end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                   def parse_header()
         
     | 
| 
      
 40 
     | 
    
         
            +
                     str = @data.slice(0, HEADER_SIZE)
         
     | 
| 
      
 41 
     | 
    
         
            +
                     @properties = Joule::SRM::Properties.new
         
     | 
| 
      
 42 
     | 
    
         
            +
                     @properties.ident=str.slice(0,4)
         
     | 
| 
      
 43 
     | 
    
         
            +
                     @properties.srm_date = str.slice(4,2).unpack('S')[0]
         
     | 
| 
      
 44 
     | 
    
         
            +
                     @properties.wheel_size = str.slice(6,2).unpack('S')[0]
         
     | 
| 
      
 45 
     | 
    
         
            +
                     @properties.record_interval_numerator = str.slice(8,1).unpack('C')[0]
         
     | 
| 
      
 46 
     | 
    
         
            +
                     @properties.record_interval_denominator = str.slice(9,1).unpack('C')[0]
         
     | 
| 
      
 47 
     | 
    
         
            +
                     @properties.block_count = str.slice(10,2).unpack('S')[0]
         
     | 
| 
      
 48 
     | 
    
         
            +
                     @properties.marker_count = str.slice(12,2).unpack('S')[0]
         
     | 
| 
      
 49 
     | 
    
         
            +
                     @properties.comment = str.slice(16,70).toutf8.strip
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                     str=@data.slice(HEADER_SIZE + 
         
     | 
| 
      
 52 
     | 
    
         
            +
                       (MARKER_SIZE * (@properties.marker_count + 1 )) + 
         
     | 
| 
      
 53 
     | 
    
         
            +
                       (BLOCK_SIZE * @properties.block_count) , 6)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                     @properties.zero_offset = str.slice(0,2).unpack('S')[0]
         
     | 
| 
      
 56 
     | 
    
         
            +
                     @properties.slope = str.slice(2,2).unpack('S')[0]
         
     | 
| 
      
 57 
     | 
    
         
            +
                     @properties.record_count = str.slice(4,2).unpack('S')[0]   
         
     | 
| 
      
 58 
     | 
    
         
            +
                   end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                   def parse_markers
         
     | 
| 
      
 61 
     | 
    
         
            +
                     marker_offset = HEADER_SIZE
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                     (@properties.marker_count + 1).times { |i|
         
     | 
| 
      
 64 
     | 
    
         
            +
                       str = @data.slice(marker_offset + (i * MARKER_SIZE), MARKER_SIZE)
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                       marker = Marker.new
         
     | 
| 
      
 67 
     | 
    
         
            +
                       marker.comment = str.slice(0, 255).strip
         
     | 
| 
      
 68 
     | 
    
         
            +
                       marker.active = str.slice(255)
         
     | 
| 
      
 69 
     | 
    
         
            +
                       marker.start = str.slice(256,2).unpack('S')[0] - 1
         
     | 
| 
      
 70 
     | 
    
         
            +
                       marker.end = str.slice(258,2).unpack('S')[0] - 1
         
     | 
| 
      
 71 
     | 
    
         
            +
                       @markers << marker        
         
     | 
| 
      
 72 
     | 
    
         
            +
                     }
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                   end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                   def parse_blocks
         
     | 
| 
      
 77 
     | 
    
         
            +
                     block_offset = HEADER_SIZE + (MARKER_SIZE * (@properties.marker_count + 1 ))
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                     @blocks = Array.new
         
     | 
| 
      
 80 
     | 
    
         
            +
                     @properties.block_count.times {|i|
         
     | 
| 
      
 81 
     | 
    
         
            +
                       str=@data.slice(block_offset + (i * BLOCK_SIZE), BLOCK_SIZE)
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                       block = Hash.new
         
     | 
| 
      
 84 
     | 
    
         
            +
                       block[:time] = str.slice(0,4).unpack('I')[0]
         
     | 
| 
      
 85 
     | 
    
         
            +
                       block[:count] = str.slice(4,2).unpack('S')[0].to_i
         
     | 
| 
      
 86 
     | 
    
         
            +
                       @blocks << block
         
     | 
| 
      
 87 
     | 
    
         
            +
                     }
         
     | 
| 
      
 88 
     | 
    
         
            +
                   end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                   def parse_data_points()
         
     | 
| 
      
 91 
     | 
    
         
            +
                     count = 0
         
     | 
| 
      
 92 
     | 
    
         
            +
                     start = HEADER_SIZE + (MARKER_SIZE * (@properties.marker_count + 1 )) + (BLOCK_SIZE * @properties.block_count) + 7
         
     | 
| 
      
 93 
     | 
    
         
            +
                     total_distance = 0
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                     while count < @properties.record_count
         
     | 
| 
      
 96 
     | 
    
         
            +
                       record=@data.slice(start + (count * 5), 5)
         
     | 
| 
      
 97 
     | 
    
         
            +
                       byte1=record.slice(0)
         
     | 
| 
      
 98 
     | 
    
         
            +
                       byte2=record.slice(1)
         
     | 
| 
      
 99 
     | 
    
         
            +
                       byte3=record.slice(2)
         
     | 
| 
      
 100 
     | 
    
         
            +
                       data_point = DataPoint.new
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                       data_point.time = count * @properties.record_interval
         
     | 
| 
      
 103 
     | 
    
         
            +
                       data_point.power = ( (byte2 & 0x0F) | (byte3 << 4) ).to_f
         
     | 
| 
      
 104 
     | 
    
         
            +
                       data_point.speed = ( ( ( (byte2 & 0xF0) << 3) | (byte1 & 0x7F) ) * 32 ) #stored in mm/s
         
     | 
| 
      
 105 
     | 
    
         
            +
                       data_point.cadence = record.slice(3)
         
     | 
| 
      
 106 
     | 
    
         
            +
                       data_point.heartrate = record.slice(4)
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                       total_distance = total_distance + (data_point.speed * @properties.record_interval) 
         
     | 
| 
      
 109 
     | 
    
         
            +
                       data_point.distance = total_distance #in mm
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                       @data_points << data_point
         
     | 
| 
      
 112 
     | 
    
         
            +
                       count=count + 1
         
     | 
| 
      
 113 
     | 
    
         
            +
                     end
         
     | 
| 
      
 114 
     | 
    
         
            +
                   end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                   def parse_data_point_times
         
     | 
| 
      
 117 
     | 
    
         
            +
                     count = 0
         
     | 
| 
      
 118 
     | 
    
         
            +
                     @blocks.each { |block|
         
     | 
| 
      
 119 
     | 
    
         
            +
                      relative_count = 0
         
     | 
| 
      
 120 
     | 
    
         
            +
                       while relative_count < block[:count]
         
     | 
| 
      
 121 
     | 
    
         
            +
                         @data_points[count].time_of_day =  block[:time]/100 + (@properties.record_interval*relative_count)
         
     | 
| 
      
 122 
     | 
    
         
            +
                         @data_points[count].time_with_pauses = block[:time]/100 - @blocks[0][:time]/100 + (@properties.record_interval*(relative_count + 1))
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                         relative_count=relative_count+1
         
     | 
| 
      
 125 
     | 
    
         
            +
                         count=count+1
         
     | 
| 
      
 126 
     | 
    
         
            +
                       end
         
     | 
| 
      
 127 
     | 
    
         
            +
                     }
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                     @properties.date_time = data_points.first.time_of_day.to_i
         
     | 
| 
      
 130 
     | 
    
         
            +
                   end
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                   def calculate_marker_values
         
     | 
| 
      
 133 
     | 
    
         
            +
                     @markers.each_with_index { |marker, i|
         
     | 
| 
      
 134 
     | 
    
         
            +
                       calculate_marker_averages marker      
         
     | 
| 
      
 135 
     | 
    
         
            +
                       calculate_marker_maximums marker
         
     | 
| 
      
 136 
     | 
    
         
            +
                       calculate_marker_training_metrics marker
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                       if i.eql?(0)
         
     | 
| 
      
 139 
     | 
    
         
            +
                         marker.distance = @data_points.last.distance
         
     | 
| 
      
 140 
     | 
    
         
            +
                       else
         
     | 
| 
      
 141 
     | 
    
         
            +
                         marker.distance = @data_points[marker.end + 1].distance - @data_points[marker.start].distance
         
     | 
| 
      
 142 
     | 
    
         
            +
                       end
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                       marker.duration_seconds = (marker.end - marker.start + 1) * @properties.record_interval
         
     | 
| 
      
 145 
     | 
    
         
            +
                       marker.energy = (marker.average_power.round * marker.duration_seconds)/1000
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                     }
         
     | 
| 
      
 148 
     | 
    
         
            +
                   end
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                 end
         
     | 
| 
      
 151 
     | 
    
         
            +
              end
         
     | 
| 
      
 152 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,31 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module SRM
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Properties
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_accessor :ident, :srm_date, :start_date_time, :wheel_size, :record_interval_numerator,
         
     | 
| 
      
 5 
     | 
    
         
            +
                    :record_interval_denominator, :block_count, :marker_count, :comment, 
         
     | 
| 
      
 6 
     | 
    
         
            +
                    :zero_offset, :record_count, :slope
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def record_interval 
         
     | 
| 
      
 9 
     | 
    
         
            +
                    return self.record_interval_numerator/self.record_interval_denominator
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  def date
         
     | 
| 
      
 13 
     | 
    
         
            +
                    Date.new(1880,1,1) + self.srm_date
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  def slope
         
     | 
| 
      
 17 
     | 
    
         
            +
                    @slope/305.58
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  def date_time=(time)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    self.start_date_time = Time.mktime(self.date.year.to_i, self.date.month.to_i, self.date.day.to_i) + time
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def date_time
         
     | 
| 
      
 25 
     | 
    
         
            +
                    self.start_date_time
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
                end  
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
            end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
    
        data/lib/joule/tcx.rb
    ADDED
    
    
| 
         @@ -0,0 +1,177 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'nokogiri'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 4 
     | 
    
         
            +
              module TCX
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Parser
         
     | 
| 
      
 6 
     | 
    
         
            +
                  include Joule::Calculator::MarkerCalculator
         
     | 
| 
      
 7 
     | 
    
         
            +
                  include Joule::Calculator::PeakPowerCalculator
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  attr_reader :data_points, :markers, :properties, :peak_powers
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def initialize(string_or_io)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @string_or_io = string_or_io
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @data_points = Array.new
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @properties = Joule::TCX::Properties.new
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @markers = Array.new
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @peak_powers = Array.new
         
     | 
| 
      
 17 
     | 
    
         
            +
                    @has_native_speed = false
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  def parse(options = {})
         
     | 
| 
      
 21 
     | 
    
         
            +
                    @properties.record_interval = 1
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @total_record_count = 0
         
     | 
| 
      
 23 
     | 
    
         
            +
                    parse_activity("Biking")
         
     | 
| 
      
 24 
     | 
    
         
            +
                    create_workout_marker()
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    if(options[:calculate_marker_values])
         
     | 
| 
      
 27 
     | 
    
         
            +
                      calculate_marker_values()
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    if(options[:calculate_peak_power_values])
         
     | 
| 
      
 31 
     | 
    
         
            +
                      calculate_peak_power_values(:durations => options[:durations], :total_duration => @markers.first.duration_seconds)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  private  
         
     | 
| 
      
 37 
     | 
    
         
            +
                  def create_workout_marker
         
     | 
| 
      
 38 
     | 
    
         
            +
                    if(@markers.size > 1)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      @markers << Marker.new(:start => 0, :end => @data_points.size - 1)
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  def parse_activity(sport)
         
     | 
| 
      
 44 
     | 
    
         
            +
                    document = Nokogiri::XML::Document.parse(@string_or_io)
         
     | 
| 
      
 45 
     | 
    
         
            +
                    document.xpath("//xmlns:Activity[@Sport='#{sport}']").each do |activity|
         
     | 
| 
      
 46 
     | 
    
         
            +
                      @properties.id = activity.at("./xmlns:Id").content
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                      activity.children.each do |child|
         
     | 
| 
      
 49 
     | 
    
         
            +
                        parse_lap(child) if child.name == "Lap"
         
     | 
| 
      
 50 
     | 
    
         
            +
                      end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                      calculate_speed if(!@has_native_speed)
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  def parse_lap(lap_node)
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                    marker = Marker.new
         
     | 
| 
      
 60 
     | 
    
         
            +
                    marker.start_time = DateTime.parse(lap_node.attribute("StartTime").content)
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                    if(@markers.size == 0)
         
     | 
| 
      
 63 
     | 
    
         
            +
                      @properties.start_date_time = marker.start_time
         
     | 
| 
      
 64 
     | 
    
         
            +
                    end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    if @data_points.size > 0
         
     | 
| 
      
 67 
     | 
    
         
            +
                      marker.start = @data_points.last.time + 1
         
     | 
| 
      
 68 
     | 
    
         
            +
                    else
         
     | 
| 
      
 69 
     | 
    
         
            +
                      marker.start= 0
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                    lap_node.children.each do |child|
         
     | 
| 
      
 73 
     | 
    
         
            +
                      marker.duration_seconds = child.content.to_i if child.name == "TotalTimeSeconds" 
         
     | 
| 
      
 74 
     | 
    
         
            +
                      # puts "Distance in meters: #{child.content}" if child.name == "DistanceMeters"
         
     | 
| 
      
 75 
     | 
    
         
            +
                      # puts "Maximum Speed: #{child.content}" if child.name == "MaximumSpeed"
         
     | 
| 
      
 76 
     | 
    
         
            +
                      # puts "Calories: #{child.content}" if child.name == "Calories"
         
     | 
| 
      
 77 
     | 
    
         
            +
                      # puts "Intensity: #{child.content}" if child.name == "Intensity"
         
     | 
| 
      
 78 
     | 
    
         
            +
                      # puts "Cadence: #{child.content}" if child.name == "Cadence"  
         
     | 
| 
      
 79 
     | 
    
         
            +
                      # puts "Trigger Method: #{child.content}" if child.name == "TriggerMethod"  
         
     | 
| 
      
 80 
     | 
    
         
            +
                      parse_track(child) if(child.name == "Track")
         
     | 
| 
      
 81 
     | 
    
         
            +
                    end
         
     | 
| 
      
 82 
     | 
    
         
            +
                    marker.end = @data_points.last.time
         
     | 
| 
      
 83 
     | 
    
         
            +
                    @markers << marker
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  def parse_track(track)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    @trackpoint_count = 0
         
     | 
| 
      
 88 
     | 
    
         
            +
                    track.children.each do |trackpoint|
         
     | 
| 
      
 89 
     | 
    
         
            +
                      parse_trackpoint(trackpoint) if(trackpoint.name == "Trackpoint")
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                    end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                  def parse_trackpoint(trackpoint)
         
     | 
| 
      
 96 
     | 
    
         
            +
                    data_point = DataPoint.new
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                    trackpoint.children.each do |data|
         
     | 
| 
      
 99 
     | 
    
         
            +
                      parse_times(data, data_point) if(data.name == "Time")
         
     | 
| 
      
 100 
     | 
    
         
            +
                      data_point.altitude = data.content.to_f if data.name == "AltitudeMeters"
         
     | 
| 
      
 101 
     | 
    
         
            +
                      data_point.distance = (data.content.to_f * 1000) if data.name == "DistanceMeters"
         
     | 
| 
      
 102 
     | 
    
         
            +
                      data_point.cadence = data.content.to_i if data.name == "Cadence"
         
     | 
| 
      
 103 
     | 
    
         
            +
                      parse_heartrate(data, data_point) if data.name == "HeartRateBpm"
         
     | 
| 
      
 104 
     | 
    
         
            +
                      parse_extensions(data, data_point) if data.name == "Extensions"
         
     | 
| 
      
 105 
     | 
    
         
            +
                      parse_position(data, data_point) if data.name == "Position" 
         
     | 
| 
      
 106 
     | 
    
         
            +
                    end
         
     | 
| 
      
 107 
     | 
    
         
            +
                    @data_points << data_point
         
     | 
| 
      
 108 
     | 
    
         
            +
                    @trackpoint_count = @trackpoint_count + 1
         
     | 
| 
      
 109 
     | 
    
         
            +
                    @total_record_count = @total_record_count + 1
         
     | 
| 
      
 110 
     | 
    
         
            +
                  end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                  def parse_times(data, data_point)
         
     | 
| 
      
 113 
     | 
    
         
            +
                    time_of_day =  DateTime.parse(data.content)
         
     | 
| 
      
 114 
     | 
    
         
            +
                    data_point.time_of_day = (time_of_day.hour * 3600) + (time_of_day.min * 60) + time_of_day.sec
         
     | 
| 
      
 115 
     | 
    
         
            +
                    data_point.time = @total_record_count * @properties.record_interval
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                    if(@trackpoint_count == 0)
         
     | 
| 
      
 118 
     | 
    
         
            +
                      track_start_time = data_point.time_of_day
         
     | 
| 
      
 119 
     | 
    
         
            +
                      @track_offset_in_seconds = track_start_time - @properties.start_time_in_seconds
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
                    data_point.time_with_pauses = (@trackpoint_count * @properties.record_interval) + @track_offset_in_seconds
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                  def parse_heartrate(heartrate, data_point)
         
     | 
| 
      
 125 
     | 
    
         
            +
                    heartrate.children.each do |child|
         
     | 
| 
      
 126 
     | 
    
         
            +
                      data_point.heartrate = child.content.to_i if child.name == "Value"
         
     | 
| 
      
 127 
     | 
    
         
            +
                    end
         
     | 
| 
      
 128 
     | 
    
         
            +
                  end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                  def parse_extensions(extensions, data_point)
         
     | 
| 
      
 131 
     | 
    
         
            +
                    extensions.children.each do |extension|
         
     | 
| 
      
 132 
     | 
    
         
            +
                      extension.children.each do |tpx|
         
     | 
| 
      
 133 
     | 
    
         
            +
                        (data_point.speed = tpx.content.to_f; @has_native_speed = true;) if(tpx.name == "Speed")
         
     | 
| 
      
 134 
     | 
    
         
            +
                        (data_point.power = tpx.content.to_f) if(tpx.name == "Watts")
         
     | 
| 
      
 135 
     | 
    
         
            +
                      end 
         
     | 
| 
      
 136 
     | 
    
         
            +
                    end  
         
     | 
| 
      
 137 
     | 
    
         
            +
                  end
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
      
 139 
     | 
    
         
            +
                  def parse_position(position, data_point)
         
     | 
| 
      
 140 
     | 
    
         
            +
                    position.children.each do |child|
         
     | 
| 
      
 141 
     | 
    
         
            +
                      (data_point.latitude = child.content.to_f) if child.name == "LatitudeDegrees"
         
     | 
| 
      
 142 
     | 
    
         
            +
                      (data_point.longitude = child.content.to_f) if child.name == "LongitudeDegrees"
         
     | 
| 
      
 143 
     | 
    
         
            +
                    end
         
     | 
| 
      
 144 
     | 
    
         
            +
                  end
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
                  def calculate_marker_values
         
     | 
| 
      
 147 
     | 
    
         
            +
                    @markers.each_with_index { |marker, i|
         
     | 
| 
      
 148 
     | 
    
         
            +
                      calculate_marker_averages marker      
         
     | 
| 
      
 149 
     | 
    
         
            +
                      calculate_marker_maximums marker
         
     | 
| 
      
 150 
     | 
    
         
            +
                      calculate_marker_training_metrics marker
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
                      if i.eql?(0)
         
     | 
| 
      
 153 
     | 
    
         
            +
                        marker.distance = @data_points.last.distance
         
     | 
| 
      
 154 
     | 
    
         
            +
                      else
         
     | 
| 
      
 155 
     | 
    
         
            +
                        marker.distance = @data_points[marker.end + 1].distance - @data_points[marker.start].distance
         
     | 
| 
      
 156 
     | 
    
         
            +
                      end
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                      marker.duration_seconds = (marker.end - marker.start + 1) * @properties.record_interval
         
     | 
| 
      
 159 
     | 
    
         
            +
                      marker.energy = (marker.average_power.round * marker.duration_seconds)/1000
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                    }
         
     | 
| 
      
 162 
     | 
    
         
            +
                  end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                  def calculate_speed
         
     | 
| 
      
 165 
     | 
    
         
            +
                    @data_points.each_with_index { |v, i| 
         
     | 
| 
      
 166 
     | 
    
         
            +
                      if(i == 0)
         
     | 
| 
      
 167 
     | 
    
         
            +
                        delta = v.distance 
         
     | 
| 
      
 168 
     | 
    
         
            +
                      else
         
     | 
| 
      
 169 
     | 
    
         
            +
                        delta = v.distance - @data_points[i-1].distance
         
     | 
| 
      
 170 
     | 
    
         
            +
                      end
         
     | 
| 
      
 171 
     | 
    
         
            +
                      v.speed = delta / @properties.record_interval      
         
     | 
| 
      
 172 
     | 
    
         
            +
                    }
         
     | 
| 
      
 173 
     | 
    
         
            +
                  end
         
     | 
| 
      
 174 
     | 
    
         
            +
                end
         
     | 
| 
      
 175 
     | 
    
         
            +
                
         
     | 
| 
      
 176 
     | 
    
         
            +
              end
         
     | 
| 
      
 177 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,55 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Joule
         
     | 
| 
      
 2 
     | 
    
         
            +
              module UnitsConversion
         
     | 
| 
      
 3 
     | 
    
         
            +
                def convert_speed(speed)
         
     | 
| 
      
 4 
     | 
    
         
            +
                  #convert to mm/s
         
     | 
| 
      
 5 
     | 
    
         
            +
                  if self.properties.speed_units_are_english?
         
     | 
| 
      
 6 
     | 
    
         
            +
                    miles_per_hour_to_millimeters_per_second speed
         
     | 
| 
      
 7 
     | 
    
         
            +
                  else
         
     | 
| 
      
 8 
     | 
    
         
            +
                    kilometers_per_hour_to_millimeters_per_second speed
         
     | 
| 
      
 9 
     | 
    
         
            +
                  end
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def convert_distance(distance)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  #convert distance to mm
         
     | 
| 
      
 14 
     | 
    
         
            +
                  if self.properties.distance_units_are_english?
         
     | 
| 
      
 15 
     | 
    
         
            +
                    miles_to_millimeters distance
         
     | 
| 
      
 16 
     | 
    
         
            +
                  else
         
     | 
| 
      
 17 
     | 
    
         
            +
                    kilometers_to_millimeters distance
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def miles_per_hour_to_millimeters_per_second(speed)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  speed * 447.04 
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                def millimeters_per_second_to_miles_per_hour(speed)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  speed / 447.04 
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def kilometers_per_hour_to_millimeters_per_second(speed)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  speed * 277.78 
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def millimeters_per_second_to_kilometers_per_hour(speed)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  speed / 277.78 
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                def miles_to_millimeters(distance)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  distance * 1609344
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def millimeters_to_miles(distance)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  distance / 1609344
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def kilometers_to_millimeters(distance)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  distance * 1000000
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def millimeters_to_kilometers(distance)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  distance / 1000000
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
              end  
         
     | 
| 
      
 54 
     | 
    
         
            +
            end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
    
        metadata
    ADDED
    
    | 
         @@ -0,0 +1,109 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            --- !ruby/object:Gem::Specification 
         
     | 
| 
      
 2 
     | 
    
         
            +
            name: joule
         
     | 
| 
      
 3 
     | 
    
         
            +
            version: !ruby/object:Gem::Version 
         
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.0.0
         
     | 
| 
      
 5 
     | 
    
         
            +
            platform: ruby
         
     | 
| 
      
 6 
     | 
    
         
            +
            authors: 
         
     | 
| 
      
 7 
     | 
    
         
            +
            - Andrew Olson
         
     | 
| 
      
 8 
     | 
    
         
            +
            autorequire: 
         
     | 
| 
      
 9 
     | 
    
         
            +
            bindir: bin
         
     | 
| 
      
 10 
     | 
    
         
            +
            cert_chain: []
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            date: 2010-01-05 00:00:00 -05:00
         
     | 
| 
      
 13 
     | 
    
         
            +
            default_executable: 
         
     | 
| 
      
 14 
     | 
    
         
            +
            dependencies: 
         
     | 
| 
      
 15 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency 
         
     | 
| 
      
 16 
     | 
    
         
            +
              name: nokogiri
         
     | 
| 
      
 17 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 18 
     | 
    
         
            +
              version_requirement: 
         
     | 
| 
      
 19 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement 
         
     | 
| 
      
 20 
     | 
    
         
            +
                requirements: 
         
     | 
| 
      
 21 
     | 
    
         
            +
                - - ">="
         
     | 
| 
      
 22 
     | 
    
         
            +
                  - !ruby/object:Gem::Version 
         
     | 
| 
      
 23 
     | 
    
         
            +
                    version: 1.4.1
         
     | 
| 
      
 24 
     | 
    
         
            +
                version: 
         
     | 
| 
      
 25 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency 
         
     | 
| 
      
 26 
     | 
    
         
            +
              name: fastercsv
         
     | 
| 
      
 27 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 28 
     | 
    
         
            +
              version_requirement: 
         
     | 
| 
      
 29 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement 
         
     | 
| 
      
 30 
     | 
    
         
            +
                requirements: 
         
     | 
| 
      
 31 
     | 
    
         
            +
                - - ">="
         
     | 
| 
      
 32 
     | 
    
         
            +
                  - !ruby/object:Gem::Version 
         
     | 
| 
      
 33 
     | 
    
         
            +
                    version: 1.4.0
         
     | 
| 
      
 34 
     | 
    
         
            +
                version: 
         
     | 
| 
      
 35 
     | 
    
         
            +
            description: ""
         
     | 
| 
      
 36 
     | 
    
         
            +
            email: anolson@gmail.com
         
     | 
| 
      
 37 
     | 
    
         
            +
            executables: []
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            extensions: []
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            extra_rdoc_files: 
         
     | 
| 
      
 42 
     | 
    
         
            +
            - README.rdoc
         
     | 
| 
      
 43 
     | 
    
         
            +
            files: 
         
     | 
| 
      
 44 
     | 
    
         
            +
            - lib/joule
         
     | 
| 
      
 45 
     | 
    
         
            +
            - lib/joule/array.rb
         
     | 
| 
      
 46 
     | 
    
         
            +
            - lib/joule/calculator
         
     | 
| 
      
 47 
     | 
    
         
            +
            - lib/joule/calculator/marker_calculator.rb
         
     | 
| 
      
 48 
     | 
    
         
            +
            - lib/joule/calculator/peak_power_calculator.rb
         
     | 
| 
      
 49 
     | 
    
         
            +
            - lib/joule/calculator/power_calculator.rb
         
     | 
| 
      
 50 
     | 
    
         
            +
            - lib/joule/calculator.rb
         
     | 
| 
      
 51 
     | 
    
         
            +
            - lib/joule/csv
         
     | 
| 
      
 52 
     | 
    
         
            +
            - lib/joule/csv/parser.rb
         
     | 
| 
      
 53 
     | 
    
         
            +
            - lib/joule/csv.rb
         
     | 
| 
      
 54 
     | 
    
         
            +
            - lib/joule/data_point.rb
         
     | 
| 
      
 55 
     | 
    
         
            +
            - lib/joule/float.rb
         
     | 
| 
      
 56 
     | 
    
         
            +
            - lib/joule/ibike
         
     | 
| 
      
 57 
     | 
    
         
            +
            - lib/joule/ibike/parser.rb
         
     | 
| 
      
 58 
     | 
    
         
            +
            - lib/joule/ibike/properties.rb
         
     | 
| 
      
 59 
     | 
    
         
            +
            - lib/joule/ibike.rb
         
     | 
| 
      
 60 
     | 
    
         
            +
            - lib/joule/marker.rb
         
     | 
| 
      
 61 
     | 
    
         
            +
            - lib/joule/powertap
         
     | 
| 
      
 62 
     | 
    
         
            +
            - lib/joule/powertap/parser.rb
         
     | 
| 
      
 63 
     | 
    
         
            +
            - lib/joule/powertap/properties.rb
         
     | 
| 
      
 64 
     | 
    
         
            +
            - lib/joule/powertap.rb
         
     | 
| 
      
 65 
     | 
    
         
            +
            - lib/joule/srm
         
     | 
| 
      
 66 
     | 
    
         
            +
            - lib/joule/srm/parser.rb
         
     | 
| 
      
 67 
     | 
    
         
            +
            - lib/joule/srm/properties.rb
         
     | 
| 
      
 68 
     | 
    
         
            +
            - lib/joule/srm.rb
         
     | 
| 
      
 69 
     | 
    
         
            +
            - lib/joule/tcx
         
     | 
| 
      
 70 
     | 
    
         
            +
            - lib/joule/tcx/parser.rb
         
     | 
| 
      
 71 
     | 
    
         
            +
            - lib/joule/tcx/properties.rb
         
     | 
| 
      
 72 
     | 
    
         
            +
            - lib/joule/tcx.rb
         
     | 
| 
      
 73 
     | 
    
         
            +
            - lib/joule/units_conversion.rb
         
     | 
| 
      
 74 
     | 
    
         
            +
            - lib/joule.rb
         
     | 
| 
      
 75 
     | 
    
         
            +
            - README.rdoc
         
     | 
| 
      
 76 
     | 
    
         
            +
            - Rakefile
         
     | 
| 
      
 77 
     | 
    
         
            +
            has_rdoc: true
         
     | 
| 
      
 78 
     | 
    
         
            +
            homepage: http://github.com/anolson/joule
         
     | 
| 
      
 79 
     | 
    
         
            +
            post_install_message: 
         
     | 
| 
      
 80 
     | 
    
         
            +
            rdoc_options: 
         
     | 
| 
      
 81 
     | 
    
         
            +
            - --line-numbers
         
     | 
| 
      
 82 
     | 
    
         
            +
            - --inline-source
         
     | 
| 
      
 83 
     | 
    
         
            +
            - --title
         
     | 
| 
      
 84 
     | 
    
         
            +
            - Joule
         
     | 
| 
      
 85 
     | 
    
         
            +
            - --main
         
     | 
| 
      
 86 
     | 
    
         
            +
            - README.rdoc
         
     | 
| 
      
 87 
     | 
    
         
            +
            require_paths: 
         
     | 
| 
      
 88 
     | 
    
         
            +
            - lib
         
     | 
| 
      
 89 
     | 
    
         
            +
            required_ruby_version: !ruby/object:Gem::Requirement 
         
     | 
| 
      
 90 
     | 
    
         
            +
              requirements: 
         
     | 
| 
      
 91 
     | 
    
         
            +
              - - ">="
         
     | 
| 
      
 92 
     | 
    
         
            +
                - !ruby/object:Gem::Version 
         
     | 
| 
      
 93 
     | 
    
         
            +
                  version: "0"
         
     | 
| 
      
 94 
     | 
    
         
            +
              version: 
         
     | 
| 
      
 95 
     | 
    
         
            +
            required_rubygems_version: !ruby/object:Gem::Requirement 
         
     | 
| 
      
 96 
     | 
    
         
            +
              requirements: 
         
     | 
| 
      
 97 
     | 
    
         
            +
              - - ">="
         
     | 
| 
      
 98 
     | 
    
         
            +
                - !ruby/object:Gem::Version 
         
     | 
| 
      
 99 
     | 
    
         
            +
                  version: "1.2"
         
     | 
| 
      
 100 
     | 
    
         
            +
              version: 
         
     | 
| 
      
 101 
     | 
    
         
            +
            requirements: []
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
            rubyforge_project: 
         
     | 
| 
      
 104 
     | 
    
         
            +
            rubygems_version: 1.3.1
         
     | 
| 
      
 105 
     | 
    
         
            +
            signing_key: 
         
     | 
| 
      
 106 
     | 
    
         
            +
            specification_version: 2
         
     | 
| 
      
 107 
     | 
    
         
            +
            summary: A Ruby library for parsing bicycle powermeter data.
         
     | 
| 
      
 108 
     | 
    
         
            +
            test_files: []
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     |