joule 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,15 +4,60 @@
4
4
 
5
5
  Joule is a Ruby library for parsing bicycle powermeter data.
6
6
 
7
+ The following formats are supported:
8
+
9
+ * SRM (.srm)
10
+ * PowerTap (.csv)
11
+ * iBike (.csv)
12
+ * Garmin (.tcx)
13
+
14
+ ==Install
15
+ sudo gem install joule
16
+
17
+ ==Usage
18
+ require 'joule'
19
+
20
+ parser = Joule::SRM::Parser.new(IO.read("sample_srm_file.srm"))
21
+ workout = parser.parse(
22
+ :calculate_marker_values => true,
23
+ :calculate_peak_power_values => true,
24
+ :durations => [5, 300, 1200])
25
+
26
+ # data_points
27
+ workout.data_points.each { |v|
28
+ puts "--Data Point--"
29
+ puts "Time: #{v.time}"
30
+ puts "Power: #{v.power}"
31
+ }
32
+
33
+ # markers
34
+ workout.markers.each { |v|
35
+ puts "--Marker--"
36
+ puts "Duration: #{v.duration_seconds}"
37
+ puts "Average Power: #{v.average_power}"
38
+ puts "Normalized Power: #{v.average_power}"
39
+ }
40
+
41
+ # peak_powers
42
+ workout.peak_powers.each { |v|
43
+ puts "--Peak Power--"
44
+ puts "Duration: #{v.duration}"
45
+ puts "Peak Power: #{v.value}"
46
+ }
47
+
48
+ == Dependencies
49
+
50
+ * nokogiri >= 1.4.1
51
+ * fastercsv >=1.4.0
7
52
 
8
53
  == License
9
54
 
10
55
  (The MIT License)
11
56
 
12
- Copyright (c) 2008 - 2009 Andrew Olson
57
+ Copyright (c) 2008 - 2010 Andrew Olson
13
58
 
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:
59
+ 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
60
 
16
61
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
17
62
 
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.
63
+ 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 CHANGED
@@ -9,22 +9,19 @@ task :default => [:test]
9
9
  desc 'Test Joule.'
10
10
  Rake::TestTask.new(:test) do |t|
11
11
  t.libs << 'lib'
12
- t.pattern = 'test/**/test_*.rb'
12
+ t.libs << 'test/lib'
13
+ t.pattern = 'test/**/*test.rb'
13
14
  t.verbose = true
14
15
  end
15
16
 
16
-
17
-
18
-
19
-
20
17
  spec = Gem::Specification.new do |s|
21
18
  s.name = "joule"
22
19
  s.summary = "A Ruby library for parsing bicycle powermeter data."
23
- s.description = ""
20
+ s.description = "Joule parses and does some basic analyzing of powermeter data. Supported formats include SRM(.srm), Saris PowerTap(.csv), iBike(.csv), and Garmin(.tcx)"
24
21
  s.homepage = "http://github.com/anolson/joule"
25
22
 
26
- s.version = "1.0.0"
27
- s.date = "2010-1-5"
23
+ s.version = "1.0.1"
24
+ s.date = "2010-9-15"
28
25
 
29
26
  s.authors = ["Andrew Olson"]
30
27
  s.email = "anolson@gmail.com"
@@ -46,3 +43,11 @@ end
46
43
  Rake::GemPackageTask.new(spec) do |pkg|
47
44
  pkg.need_tar = true
48
45
  end
46
+
47
+ Rake::RDocTask.new do |doc|
48
+ doc.main = "README.rdoc"
49
+ doc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
50
+ doc.rdoc_dir = "doc"
51
+ end
52
+
53
+
@@ -1,14 +1,34 @@
1
1
  require 'joule/array'
2
- require 'joule/float'
2
+ require 'joule/exception'
3
+ require 'joule/hashable'
3
4
 
4
5
  require 'joule/data_point'
5
6
  require 'joule/marker'
6
7
  require 'joule/calculator'
8
+ require 'joule/peak_power'
7
9
  require 'joule/units_conversion'
10
+ require 'joule/workout'
8
11
 
12
+ require 'joule/base'
9
13
  require 'joule/csv'
10
14
  require 'joule/ibike'
11
15
  require 'joule/powertap'
12
16
  require 'joule/srm'
13
17
  require 'joule/tcx'
14
18
 
19
+ module Joule
20
+
21
+ def Joule.parser(extension, data)
22
+ if(extension.eql?(Joule::SRM::FILE_EXTENSION))
23
+ Joule::SRM::Parser.new(data)
24
+ elsif(extension.eql?(Joule::TCX::FILE_EXTENSION))
25
+ Joule::TCX::Parser.new(data)
26
+ elsif(extension.eql?(Joule::CSV::FILE_EXTENSION))
27
+ Joule::CSV.parser(extension, data)
28
+ else
29
+ raise UnsupportedFileTypeException
30
+ end
31
+ end
32
+
33
+ end
34
+
@@ -0,0 +1 @@
1
+ require 'joule/base/parser'
@@ -0,0 +1,50 @@
1
+ module Joule
2
+ module Base
3
+ class Parser
4
+ include Joule::Calculator::MarkerCalculator
5
+ include Joule::Calculator::PeakPowerCalculator
6
+
7
+ attr_accessor :workout
8
+
9
+ def initialize(data)
10
+ @data = data
11
+ @workout = Workout.new
12
+ end
13
+
14
+ # Parse powermeter data.
15
+ # == Options
16
+ #
17
+ # * <tt>:calculate_marker_values </tt> - calculate the totals, averages, and maximum values for each Marker.
18
+ #
19
+ # * <tt>:calculate_peak_power_values </tt> - calculate the PeakPower values for the Workout.
20
+ #
21
+ # * <tt>:durations </tt> - an Array of durations (in seconds) of the PeakPower values that you want to calculate. Required if :calculate_peak_power_values => true.
22
+ #
23
+
24
+
25
+ def parse(options = {})
26
+
27
+ parse_properties
28
+ parse_workout
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
+ @workout
38
+ end
39
+
40
+ def parse_workout
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def parse_properties
45
+ raise NotImplementedError
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -4,35 +4,62 @@ module Joule
4
4
 
5
5
  def calculate_marker_averages(marker)
6
6
  marker.average_power = Joule::Calculator::PowerCalculator::average(
7
- self.data_points[marker.start..marker.end].collect() {|v| v.power})
7
+ @workout.data_points[marker.start..marker.end].collect() {|v| v.power})
8
8
 
9
9
  marker.average_speed = Joule::Calculator::PowerCalculator::average(
10
- self.data_points[marker.start..marker.end].collect() {|v| v.speed})
10
+ @workout.data_points[marker.start..marker.end].collect() {|v| v.speed})
11
11
 
12
12
  marker.average_cadence = Joule::Calculator::PowerCalculator::average(
13
- self.data_points[marker.start..marker.end].collect() {|v| v.cadence})
13
+ @workout.data_points[marker.start..marker.end].collect() {|v| v.cadence})
14
14
 
15
15
  marker.average_heartrate = Joule::Calculator::PowerCalculator::average(
16
- self.data_points[marker.start..marker.end].collect() {|v| v.heartrate})
16
+ @workout.data_points[marker.start..marker.end].collect() {|v| v.heartrate})
17
17
  end
18
18
 
19
19
  def calculate_marker_maximums(marker)
20
20
  marker.maximum_power = Joule::Calculator::PowerCalculator::maximum(
21
- self.data_points[marker.start..marker.end].collect() {|value| value.power})
21
+ @workout.data_points[marker.start..marker.end].collect() {|value| value.power})
22
22
 
23
23
  marker.maximum_speed = Joule::Calculator::PowerCalculator::maximum(
24
- self.data_points[marker.start..marker.end].collect() {|value| value.speed})
24
+ @workout.data_points[marker.start..marker.end].collect() {|value| value.speed})
25
25
 
26
26
  marker.maximum_cadence = Joule::Calculator::PowerCalculator::maximum(
27
- self.data_points[marker.start..marker.end].collect() {|value| value.cadence})
27
+ @workout.data_points[marker.start..marker.end].collect() {|value| value.cadence})
28
28
 
29
29
  marker.maximum_heartrate = Joule::Calculator::PowerCalculator::maximum(
30
- self.data_points[marker.start..marker.end].collect() {|value| value.heartrate})
30
+ @workout.data_points[marker.start..marker.end].collect() {|value| value.heartrate})
31
31
  end
32
32
 
33
33
  def calculate_marker_training_metrics(marker)
34
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)
35
+ @workout.data_points[marker.start..marker.end].collect() {|value| value.power}, @workout.properties.record_interval)
36
+ end
37
+
38
+
39
+ def calculate_marker_totals(marker, index)
40
+ if(index == 0)
41
+ marker.distance = @workout.data_points.last.distance
42
+ marker.duration_seconds = @workout.data_points.last.time
43
+ else
44
+ if(marker.start == 0)
45
+ marker.distance = @workout.data_points[marker.end].distance - @workout.data_points[marker.start].distance
46
+ marker.duration_seconds = @workout.data_points[marker.end].time - @workout.data_points[marker.start].time
47
+ else
48
+ marker.distance = @workout.data_points[marker.end].distance - @workout.data_points[marker.start - 1].distance
49
+ marker.duration_seconds = @workout.data_points[marker.end].time - @workout.data_points[marker.start - 1].time
50
+ end
51
+ end
52
+
53
+ marker.energy = (marker.average_power.round * marker.duration_seconds)/1000
54
+ end
55
+
56
+ def calculate_marker_values(options = {})
57
+ @workout.markers.each_with_index { |marker, i|
58
+ calculate_marker_averages marker
59
+ calculate_marker_maximums marker
60
+ calculate_marker_training_metrics marker
61
+ calculate_marker_totals marker, i
62
+ }
36
63
  end
37
64
 
38
65
  end
@@ -3,24 +3,19 @@ module Joule
3
3
  module PeakPowerCalculator
4
4
 
5
5
  def calculate_peak_power_values(options = {})
6
- power_values = @data_points.collect{|v| v.power}
6
+ array = @workout.data_points.collect{|v| v.power}
7
7
  options[:durations].each { |duration|
8
- @peak_powers << calculate_peak_power_value(duration, options[:total_duration], power_values)
8
+ @workout.peak_powers << calculate_peak_power_value(array, duration, options[:total_duration])
9
9
  }
10
-
11
10
  end
12
11
 
13
- def calculate_peak_power_value(duration, total_duration, power_values)
12
+ def calculate_peak_power_value(array, duration, total_duration)
14
13
  if duration > total_duration
15
- { :duration => duration,
16
- :value => 0,
17
- :start => 0 }
14
+ PeakPower.new(duration)
18
15
  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] }
16
+ Joule::PowerCalculator::peak_power(array, duration, (duration/@workout.properties.record_interval))
23
17
  end
18
+
24
19
  end
25
20
 
26
21
  end
@@ -13,8 +13,12 @@ module Joule
13
13
  values.sum
14
14
  end
15
15
 
16
- def self.peak_power(values, size)
17
- values.average_maximum size
16
+ def self.peak_power(array, duration, size)
17
+ average_maximum = array.average_maximum size
18
+ peak_power = PeakPower.new(duration)
19
+ peak_power.start = average_maximum[:start]
20
+ peak_power.value = average_maximum[:value]
21
+ peak_power
18
22
  end
19
23
 
20
24
  def self.training_stress_score(duration_seconds, threshold_power)
@@ -1 +1,18 @@
1
- require 'joule/csv/parser'
1
+ require 'fastercsv'
2
+ require 'joule/csv/parser'
3
+
4
+ module Joule
5
+ module CSV
6
+ FILE_EXTENSION = "csv"
7
+
8
+ def CSV.parser(extension, data)
9
+ header = FasterCSV.parse(data).shift
10
+ if header[0].to_s.downcase.eql?("ibike")
11
+ Joule::IBike::Parser.new(data)
12
+ else
13
+ Joule::PowerTap::Parser.new(data)
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -1,44 +1,11 @@
1
- require 'fastercsv'
2
-
3
1
  module Joule
4
2
  module CSV
5
- class Parser
6
- include Joule::Calculator::MarkerCalculator
7
- include Joule::Calculator::PeakPowerCalculator
3
+ class Parser < Joule::Base::Parser
8
4
  include Joule::UnitsConversion
9
5
 
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
6
+ def parse_workout()
31
7
  parse_markers
32
8
  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
9
  end
43
10
 
44
11
  protected
@@ -46,23 +13,6 @@ module Joule
46
13
  Marker.new(:start => 0, :end => records.size - 1, :comment => "")
47
14
  end
48
15
 
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
16
  end
67
17
  end
68
18
  end
@@ -1,19 +1,32 @@
1
1
  module Joule
2
2
  class DataPoint
3
- attr_accessor :altitude, :cadence, :distance, :heartrate, :latitude, :longitude, :power, :speed, :time, :time_of_day, :time_with_pauses, :torque
3
+ include Joule::Hashable
4
+
5
+ attr_accessor :altitude
6
+ attr_accessor :cadence
7
+ attr_accessor :distance
8
+ attr_accessor :heartrate
9
+ attr_accessor :latitude
10
+ attr_accessor :longitude
11
+ attr_accessor :power
12
+ attr_accessor :speed
13
+ attr_accessor :time
14
+ attr_accessor :time_of_day
15
+ attr_accessor :time_with_pauses
16
+ attr_accessor :torque
4
17
 
5
18
  def initialize()
6
- @time_of_day = 0
7
- @time = 0
8
- @time_with_pauses=0
9
- @power = 0.0
10
- @speed = 0.0
19
+ @altitude = 0.0
11
20
  @cadence = 0
12
21
  @distance = 0.0
13
- @altitude = 0.0
22
+ @heartrate = 0
14
23
  @latitude = 0.0
15
24
  @longitude = 0.0
16
- @heartrate = 0
25
+ @power = 0.0
26
+ @speed = 0.0
27
+ @time = 0
28
+ @time_of_day = 0
29
+ @time_with_pauses= 0
17
30
  @torque = 0.0
18
31
  end
19
32
  end
@@ -0,0 +1 @@
1
+ require 'joule/exception/unsupported_file_type_exception.rb'