joule 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +48 -3
- data/Rakefile +13 -8
- data/lib/joule.rb +21 -1
- data/lib/joule/base.rb +1 -0
- data/lib/joule/base/parser.rb +50 -0
- data/lib/joule/calculator/marker_calculator.rb +36 -9
- data/lib/joule/calculator/peak_power_calculator.rb +6 -11
- data/lib/joule/calculator/power_calculator.rb +6 -2
- data/lib/joule/csv.rb +18 -1
- data/lib/joule/csv/parser.rb +2 -52
- data/lib/joule/data_point.rb +21 -8
- data/lib/joule/exception.rb +1 -0
- data/lib/joule/exception/unsupported_file_type_exception.rb +7 -0
- data/lib/joule/hashable.rb +9 -0
- data/lib/joule/ibike/parser.rb +22 -22
- data/lib/joule/marker.rb +51 -51
- data/lib/joule/peak_power.rb +13 -0
- data/lib/joule/powertap/parser.rb +15 -14
- data/lib/joule/srm.rb +7 -1
- data/lib/joule/srm/parser.rb +32 -71
- data/lib/joule/tcx.rb +7 -1
- data/lib/joule/tcx/parser.rb +26 -71
- data/lib/joule/tcx/properties.rb +4 -2
- data/lib/joule/units_conversion.rb +2 -2
- data/lib/joule/workout.rb +29 -0
- metadata +49 -23
- data/lib/joule/float.rb +0 -39
data/lib/joule/ibike/parser.rb
CHANGED
@@ -15,50 +15,50 @@ module Joule
|
|
15
15
|
LONGITUDE = 13
|
16
16
|
TIMESTAMP = 14
|
17
17
|
|
18
|
-
def
|
18
|
+
def parse_properties()
|
19
19
|
records = FasterCSV.parse(@data)
|
20
20
|
header = records.shift
|
21
21
|
|
22
|
-
@properties = Joule::IBike::Properties.new
|
23
|
-
@properties.version=header[1]
|
24
|
-
@properties.units=header[2]
|
22
|
+
@workout.properties = Joule::IBike::Properties.new
|
23
|
+
@workout.properties.version=header[1]
|
24
|
+
@workout.properties.units=header[2]
|
25
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)
|
26
|
+
@workout.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
27
|
records.shift
|
28
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]
|
29
|
+
@workout.properties.total_weight = header[0]
|
30
|
+
@workout.properties.energy = header[1]
|
31
|
+
@workout.properties.record_interval = header[4].to_i
|
32
|
+
@workout.properties.starting_elevation = header[5]
|
33
|
+
@workout.properties.total_climbing = header[6]
|
34
|
+
@workout.properties.wheel_size = header[7]
|
35
|
+
@workout.properties.temperature = header[8]
|
36
|
+
@workout.properties.starting_pressure = header[9]
|
37
|
+
@workout.properties.wind_scaling = header[10]
|
38
|
+
@workout.properties.riding_tilt = header[11]
|
39
|
+
@workout.properties.calibration_weight = header[12]
|
40
|
+
@workout.properties.cm = header[13]
|
41
|
+
@workout.properties.cda = header[14]
|
42
|
+
@workout.properties.crr = header[15]
|
43
43
|
end
|
44
44
|
|
45
45
|
def parse_data_points()
|
46
46
|
records = FasterCSV.parse(@data).slice(5..-1)
|
47
47
|
records.each_with_index { |record, index|
|
48
48
|
data_point = DataPoint.new
|
49
|
-
data_point.time = index * @properties.record_interval
|
49
|
+
data_point.time = index * @workout.properties.record_interval
|
50
50
|
data_point.speed = convert_speed(record[SPEED].to_f)
|
51
51
|
data_point.power = record[POWER].to_f
|
52
52
|
data_point.distance = convert_distance(record[DISTANCE].to_f)
|
53
53
|
data_point.cadence = record[CADENCE].to_i
|
54
54
|
data_point.heartrate = record[HEARTRATE].to_i
|
55
|
-
@data_points << data_point
|
55
|
+
@workout.data_points << data_point
|
56
56
|
}
|
57
57
|
end
|
58
58
|
|
59
59
|
def parse_markers
|
60
60
|
records = FasterCSV.parse(@data).slice(5..-1)
|
61
|
-
@markers << create_workout_marker(records)
|
61
|
+
@workout.markers << create_workout_marker(records)
|
62
62
|
end
|
63
63
|
|
64
64
|
end
|
data/lib/joule/marker.rb
CHANGED
@@ -1,53 +1,53 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
1
|
+
module Joule
|
2
|
+
class Marker
|
3
|
+
include Joule::Hashable
|
4
|
+
|
5
|
+
attr_accessor :active
|
6
|
+
attr_accessor :average_cadence
|
7
|
+
attr_accessor :average_heartrate
|
8
|
+
attr_accessor :average_power
|
9
|
+
attr_accessor :average_power_to_weight
|
10
|
+
attr_accessor :average_speed
|
11
|
+
attr_accessor :comment
|
12
|
+
attr_accessor :duration_seconds
|
13
|
+
attr_accessor :distance
|
14
|
+
attr_accessor :end
|
15
|
+
attr_accessor :energy
|
16
|
+
attr_accessor :intensity_factor
|
17
|
+
attr_accessor :maximum_cadence
|
18
|
+
attr_accessor :maximum_heartrate
|
19
|
+
attr_accessor :maximum_power
|
20
|
+
attr_accessor :maximum_power_to_weight
|
21
|
+
attr_accessor :maximum_speed
|
22
|
+
attr_accessor :normalized_power
|
23
|
+
attr_accessor :start
|
24
|
+
attr_accessor :training_stress_score
|
25
|
+
|
26
|
+
|
27
|
+
def initialize(options = {})
|
28
|
+
@active = true
|
29
|
+
@average_cadence = 0
|
30
|
+
@average_heartrate = 0
|
31
|
+
@average_power = 0.0
|
32
|
+
@average_power_to_weight = 0.0
|
33
|
+
@average_speed = 0.0
|
34
|
+
@comment = ""
|
35
|
+
@duration_seconds = 0
|
36
|
+
@distance = 0.0
|
37
|
+
options[:end] ? @end = options[:end] : @end = 0
|
38
|
+
@energy = 0
|
39
|
+
@intensity_factor = 0
|
40
|
+
@maximum_cadence = 0
|
41
|
+
@maximum_heartrate = 0
|
42
|
+
@maximum_power = 0.0
|
43
|
+
@maximum_power_to_weight = 0.0
|
44
|
+
@maximum_speed = 0.0
|
45
|
+
@normalized_power = 0
|
46
|
+
options[:start] ? @start = options[:start] : @start = 0
|
47
|
+
@training_stress_score = 0.0
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
52
51
|
end
|
53
52
|
|
53
|
+
|
@@ -10,16 +10,17 @@ module Joule
|
|
10
10
|
HEARTRATE = 6
|
11
11
|
MARKER = 7
|
12
12
|
|
13
|
-
def
|
13
|
+
def parse_properties()
|
14
14
|
header = FasterCSV.parse(@data).shift
|
15
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
|
16
|
+
@workout.properties = Joule::PowerTap::Properties.new
|
17
|
+
@workout.properties.speed_units = header[SPEED].to_s.downcase
|
18
|
+
@workout.properties.power_units = header[POWER].to_s.downcase
|
19
|
+
@workout.properties.distance_units = header[DISTANCE].to_s.downcase
|
20
20
|
calculate_record_interval(records)
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
|
+
private
|
23
24
|
def parse_data_points()
|
24
25
|
records = FasterCSV.parse(@data)
|
25
26
|
records.shift
|
@@ -34,14 +35,14 @@ module Joule
|
|
34
35
|
data_point.cadence = record[CADENCE].to_i
|
35
36
|
data_point.heartrate = (record[HEARTRATE].to_i < 0) && 0 || record[HEARTRATE].to_i
|
36
37
|
|
37
|
-
@data_points << data_point
|
38
|
+
@workout.data_points << data_point
|
38
39
|
}
|
39
40
|
end
|
40
41
|
|
41
42
|
def parse_markers
|
42
43
|
records = FasterCSV.parse(@data)
|
43
44
|
records.shift
|
44
|
-
@markers << create_workout_marker(records)
|
45
|
+
@workout.markers << create_workout_marker(records)
|
45
46
|
|
46
47
|
current_marker_index = 0
|
47
48
|
|
@@ -57,23 +58,23 @@ module Joule
|
|
57
58
|
end
|
58
59
|
|
59
60
|
def create_marker(start)
|
60
|
-
if(@markers.size.eql?(1))
|
61
|
-
@markers << Marker.new(:start => 0)
|
61
|
+
if(@workout.markers.size.eql?(1))
|
62
|
+
@workout.markers << Marker.new(:start => 0)
|
62
63
|
end
|
63
64
|
set_previous_marker_end(start - 1)
|
64
|
-
@markers << Marker.new(:start => start)
|
65
|
+
@workout.markers << Marker.new(:start => start)
|
65
66
|
end
|
66
67
|
|
67
68
|
def set_previous_marker_end(value)
|
68
|
-
if(@markers.size > 1)
|
69
|
-
@markers.last.end = value
|
69
|
+
if(@workout.markers.size > 1)
|
70
|
+
@workout.markers.last.end = value
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
74
|
def calculate_record_interval(records)
|
74
75
|
times = Array.new
|
75
76
|
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
|
+
@workout.properties.record_interval = times.average.round
|
77
78
|
end
|
78
79
|
|
79
80
|
end
|
data/lib/joule/srm.rb
CHANGED
data/lib/joule/srm/parser.rb
CHANGED
@@ -2,65 +2,44 @@ require 'kconv'
|
|
2
2
|
|
3
3
|
module Joule
|
4
4
|
module SRM
|
5
|
-
class Parser
|
6
|
-
include Joule::Calculator::MarkerCalculator
|
7
|
-
include Joule::Calculator::PeakPowerCalculator
|
8
|
-
|
9
|
-
SRM = '.srm'
|
5
|
+
class Parser < Joule::Base::Parser
|
10
6
|
HEADER_SIZE=86
|
11
7
|
MARKER_SIZE=270
|
12
8
|
BLOCK_SIZE=6
|
13
9
|
|
14
|
-
|
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
|
10
|
+
def parse_workout()
|
25
11
|
parse_markers
|
26
12
|
parse_blocks
|
27
13
|
parse_data_points
|
28
14
|
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
15
|
end
|
38
|
-
|
39
|
-
def
|
16
|
+
|
17
|
+
def parse_properties()
|
40
18
|
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
|
19
|
+
@workout.properties = Joule::SRM::Properties.new
|
20
|
+
@workout.properties.ident=str.slice(0,4)
|
21
|
+
@workout.properties.srm_date = str.slice(4,2).unpack('S')[0]
|
22
|
+
@workout.properties.wheel_size = str.slice(6,2).unpack('S')[0]
|
23
|
+
@workout.properties.record_interval_numerator = str.slice(8,1).unpack('C')[0]
|
24
|
+
@workout.properties.record_interval_denominator = str.slice(9,1).unpack('C')[0]
|
25
|
+
@workout.properties.block_count = str.slice(10,2).unpack('S')[0]
|
26
|
+
@workout.properties.marker_count = str.slice(12,2).unpack('S')[0]
|
27
|
+
@workout.properties.comment = str.slice(16,70).toutf8.strip
|
50
28
|
|
51
29
|
str=@data.slice(HEADER_SIZE +
|
52
|
-
(MARKER_SIZE * (@properties.marker_count + 1 )) +
|
53
|
-
(BLOCK_SIZE * @properties.block_count) , 6)
|
30
|
+
(MARKER_SIZE * (@workout.properties.marker_count + 1 )) +
|
31
|
+
(BLOCK_SIZE * @workout.properties.block_count) , 6)
|
54
32
|
|
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]
|
33
|
+
@workout.properties.zero_offset = str.slice(0,2).unpack('S')[0]
|
34
|
+
@workout.properties.slope = str.slice(2,2).unpack('S')[0]
|
35
|
+
@workout.properties.record_count = str.slice(4,2).unpack('S')[0]
|
58
36
|
end
|
59
37
|
|
38
|
+
private
|
60
39
|
def parse_markers
|
61
40
|
marker_offset = HEADER_SIZE
|
62
41
|
|
63
|
-
(@properties.marker_count + 1).times { |i|
|
42
|
+
(@workout.properties.marker_count + 1).times { |i|
|
64
43
|
str = @data.slice(marker_offset + (i * MARKER_SIZE), MARKER_SIZE)
|
65
44
|
|
66
45
|
marker = Marker.new
|
@@ -68,16 +47,16 @@ module Joule
|
|
68
47
|
marker.active = str.slice(255)
|
69
48
|
marker.start = str.slice(256,2).unpack('S')[0] - 1
|
70
49
|
marker.end = str.slice(258,2).unpack('S')[0] - 1
|
71
|
-
@markers << marker
|
50
|
+
@workout.markers << marker
|
72
51
|
}
|
73
52
|
|
74
53
|
end
|
75
54
|
|
76
55
|
def parse_blocks
|
77
|
-
block_offset = HEADER_SIZE + (MARKER_SIZE * (@properties.marker_count + 1 ))
|
56
|
+
block_offset = HEADER_SIZE + (MARKER_SIZE * (@workout.properties.marker_count + 1 ))
|
78
57
|
|
79
58
|
@blocks = Array.new
|
80
|
-
@properties.block_count.times {|i|
|
59
|
+
@workout.properties.block_count.times {|i|
|
81
60
|
str=@data.slice(block_offset + (i * BLOCK_SIZE), BLOCK_SIZE)
|
82
61
|
|
83
62
|
block = Hash.new
|
@@ -89,26 +68,26 @@ module Joule
|
|
89
68
|
|
90
69
|
def parse_data_points()
|
91
70
|
count = 0
|
92
|
-
start = HEADER_SIZE + (MARKER_SIZE * (@properties.marker_count + 1 )) + (BLOCK_SIZE * @properties.block_count) + 7
|
93
|
-
total_distance = 0
|
71
|
+
start = HEADER_SIZE + (MARKER_SIZE * (@workout.properties.marker_count + 1 )) + (BLOCK_SIZE * @workout.properties.block_count) + 7
|
72
|
+
total_distance = 0.0
|
94
73
|
|
95
|
-
while count < @properties.record_count
|
74
|
+
while count < @workout.properties.record_count
|
96
75
|
record=@data.slice(start + (count * 5), 5)
|
97
76
|
byte1=record.slice(0)
|
98
77
|
byte2=record.slice(1)
|
99
78
|
byte3=record.slice(2)
|
100
79
|
data_point = DataPoint.new
|
101
80
|
|
102
|
-
data_point.time = count * @properties.record_interval
|
81
|
+
data_point.time = count * @workout.properties.record_interval
|
103
82
|
data_point.power = ( (byte2 & 0x0F) | (byte3 << 4) ).to_f
|
104
83
|
data_point.speed = ( ( ( (byte2 & 0xF0) << 3) | (byte1 & 0x7F) ) * 32 ) #stored in mm/s
|
105
84
|
data_point.cadence = record.slice(3)
|
106
85
|
data_point.heartrate = record.slice(4)
|
107
86
|
|
108
|
-
total_distance = total_distance + (data_point.speed * @properties.record_interval)
|
87
|
+
total_distance = total_distance + (data_point.speed * @workout.properties.record_interval)
|
109
88
|
data_point.distance = total_distance #in mm
|
110
89
|
|
111
|
-
@data_points << data_point
|
90
|
+
@workout.data_points << data_point
|
112
91
|
count=count + 1
|
113
92
|
end
|
114
93
|
end
|
@@ -118,33 +97,15 @@ module Joule
|
|
118
97
|
@blocks.each { |block|
|
119
98
|
relative_count = 0
|
120
99
|
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))
|
100
|
+
@workout.data_points[count].time_of_day = block[:time]/100 + (@workout.properties.record_interval*relative_count)
|
101
|
+
@workout.data_points[count].time_with_pauses = block[:time]/100 - @blocks[0][:time]/100 + (@workout.properties.record_interval*(relative_count + 1))
|
123
102
|
|
124
103
|
relative_count=relative_count+1
|
125
104
|
count=count+1
|
126
105
|
end
|
127
106
|
}
|
128
107
|
|
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
|
-
}
|
108
|
+
@workout.properties.date_time = @workout.data_points.first.time_of_day.to_i
|
148
109
|
end
|
149
110
|
|
150
111
|
end
|