joule 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|