hrmparser 0.6.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/.gitignore +6 -0
- data/CHANGELOG.txt +35 -0
- data/README.rdoc +42 -0
- data/Rakefile +64 -0
- data/VERSION.yml +4 -0
- data/hrmparser.gemspec +61 -0
- data/lib/hrmparser/arraymath.rb +25 -0
- data/lib/hrmparser/importer/garmin.rb +89 -0
- data/lib/hrmparser/importer/gpx.rb +51 -0
- data/lib/hrmparser/importer/polar.rb +103 -0
- data/lib/hrmparser/importer/suunto.rb +102 -0
- data/lib/hrmparser/importer/timex.rb +118 -0
- data/lib/hrmparser/importer.rb +38 -0
- data/lib/hrmparser/trackpoint.rb +50 -0
- data/lib/hrmparser/workout.rb +66 -0
- data/lib/hrmparser.rb +12 -0
- data/spec/arraymath_spec.rb +26 -0
- data/spec/hrmparser_spec.rb +298 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- metadata +76 -0
data/CHANGELOG.txt
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
0.6.0 - August 15, 2009
|
2
|
+
Add support for Timex
|
3
|
+
|
4
|
+
0.5.0 - July 25, 2009
|
5
|
+
Handle stop time in GPX
|
6
|
+
|
7
|
+
0.4.9 - June 10, 2009
|
8
|
+
Support TCX with only LAT and LNG
|
9
|
+
|
10
|
+
0.4.8 - May 23, 2009
|
11
|
+
Fixed importing when TCX LAP duration are totally completely incredibly insanely wrong
|
12
|
+
|
13
|
+
0.4.6 - May 11th, 2009
|
14
|
+
Added support for Suunto full variables including speed, distance, etc.
|
15
|
+
|
16
|
+
0.4.4 - May 10th, 2009
|
17
|
+
Fixed time-zone offset in parsing suunto data.
|
18
|
+
|
19
|
+
0.4.2 - May 9th, 2009
|
20
|
+
Fixed GPX to grab time from first trackpoint.
|
21
|
+
|
22
|
+
0.4.1 - May 9th, 2009
|
23
|
+
Added GPX support.
|
24
|
+
NOTE: all workouts do NOT account for stopped time.
|
25
|
+
duration is simply last time - first time. Will be fixed in future version.
|
26
|
+
|
27
|
+
0.4.0 - May 8th, 2009
|
28
|
+
Added initial support for Suunto T6. Just grabs HRs for now.
|
29
|
+
|
30
|
+
0.3.1 - May 6th, 2009
|
31
|
+
Handles missing data in a trackpoint now, including no lat and lng.
|
32
|
+
|
33
|
+
0.2.3
|
34
|
+
Now should keep lat and lng set to nil when they don't exist in garmin files.
|
35
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
= hrmparser
|
2
|
+
|
3
|
+
* http://github.com/teich/hrmparser/tree/master
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A ruby parser for polar and garmin hrm
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Imports garmin 405. Generates a workout object with some averages calculated.
|
12
|
+
* Not the fastest right now.
|
13
|
+
* Very very early.
|
14
|
+
|
15
|
+
== REQUIREMENTS:
|
16
|
+
|
17
|
+
* hpricot
|
18
|
+
|
19
|
+
== LICENSE:
|
20
|
+
|
21
|
+
(The MIT License)
|
22
|
+
|
23
|
+
Copyright (c) 2009 Oren Teich
|
24
|
+
|
25
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
26
|
+
a copy of this software and associated documentation files (the
|
27
|
+
'Software'), to deal in the Software without restriction, including
|
28
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
29
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
30
|
+
permit persons to whom the Software is furnished to do so, subject to
|
31
|
+
the following conditions:
|
32
|
+
|
33
|
+
The above copyright notice and this permission notice shall be
|
34
|
+
included in all copies or substantial portions of the Software.
|
35
|
+
|
36
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
37
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
38
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
39
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
40
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
41
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
42
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
#%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
5
|
+
#require File.dirname(__FILE__) + '/lib/hrmparser'
|
6
|
+
|
7
|
+
# Generate all the Rake tasks
|
8
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
9
|
+
# $hoe = Hoe.new('teich-hrmparser', HRMParser::VERSION::STRING) do |p|
|
10
|
+
# p.developer('Oren Teich', 'oren@teich.net')
|
11
|
+
# p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
12
|
+
# p.rubyforge_name = p.name # TODO this is default value
|
13
|
+
# # p.extra_deps = [
|
14
|
+
# # ['activesupport','>= 2.0.2'],
|
15
|
+
# # ]
|
16
|
+
# p.extra_dev_deps = [
|
17
|
+
# ['newgem', ">= #{::Newgem::VERSION}"]
|
18
|
+
# ]
|
19
|
+
#
|
20
|
+
# p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
21
|
+
# path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
22
|
+
# p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
23
|
+
# p.rsync_args = '-av --delete --ignore-errors'
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# require 'newgem/tasks' # load /tasks/*.rake
|
27
|
+
# Dir['tasks/**/*.rake'].each { |t| load t }
|
28
|
+
#
|
29
|
+
# # TODO - want other tests/tasks run by default? Add them to the list
|
30
|
+
# task :default => [:spec, :features]
|
31
|
+
begin
|
32
|
+
require 'jeweler'
|
33
|
+
Jeweler::Tasks.new do |gemspec|
|
34
|
+
gemspec.name = "hrmparser"
|
35
|
+
gemspec.summary = "Heart Rate Monitor Parser"
|
36
|
+
gemspec.email = "oren@teich.net"
|
37
|
+
gemspec.homepage = "http://github.com/teich/hrmparser"
|
38
|
+
gemspec.description = "Parses Polar and Garmin HRM files."
|
39
|
+
gemspec.authors = ["Oren Teich"]
|
40
|
+
|
41
|
+
gemspec.files.exclude 'spec/samples/**/*'
|
42
|
+
gemspec.test_files.exclude 'spec/samples/**/*'
|
43
|
+
|
44
|
+
end
|
45
|
+
Jeweler::GemcutterTasks.new
|
46
|
+
rescue LoadError
|
47
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
51
|
+
|
52
|
+
# require 'spec/rake/spectask'
|
53
|
+
# Spec::Rake::SpecTask.new(:spec) do |spec|
|
54
|
+
# spec.libs << 'lib' << 'spec'
|
55
|
+
# spec.spec_files = FileList['spec/**/*_spec.rb']
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# Spec::Rake::SpecTask.new(:rcov) do |spec|
|
59
|
+
# spec.libs << 'lib' << 'spec'
|
60
|
+
# spec.pattern = 'spec/**/*_spec.rb'
|
61
|
+
# spec.rcov = true
|
62
|
+
# end
|
63
|
+
|
64
|
+
task :default => :spec
|
data/VERSION.yml
ADDED
data/hrmparser.gemspec
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{hrmparser}
|
8
|
+
s.version = "0.6.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Oren Teich"]
|
12
|
+
s.date = %q{2009-12-13}
|
13
|
+
s.description = %q{Parses Polar and Garmin HRM files.}
|
14
|
+
s.email = %q{oren@teich.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"CHANGELOG.txt",
|
21
|
+
"README.rdoc",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION.yml",
|
24
|
+
"hrmparser.gemspec",
|
25
|
+
"lib/hrmparser.rb",
|
26
|
+
"lib/hrmparser/arraymath.rb",
|
27
|
+
"lib/hrmparser/importer.rb",
|
28
|
+
"lib/hrmparser/importer/garmin.rb",
|
29
|
+
"lib/hrmparser/importer/gpx.rb",
|
30
|
+
"lib/hrmparser/importer/polar.rb",
|
31
|
+
"lib/hrmparser/importer/suunto.rb",
|
32
|
+
"lib/hrmparser/importer/timex.rb",
|
33
|
+
"lib/hrmparser/trackpoint.rb",
|
34
|
+
"lib/hrmparser/workout.rb",
|
35
|
+
"spec/arraymath_spec.rb",
|
36
|
+
"spec/hrmparser_spec.rb",
|
37
|
+
"spec/spec.opts",
|
38
|
+
"spec/spec_helper.rb"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/teich/hrmparser}
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{Heart Rate Monitor Parser}
|
45
|
+
s.test_files = [
|
46
|
+
"spec/arraymath_spec.rb",
|
47
|
+
"spec/hrmparser_spec.rb",
|
48
|
+
"spec/spec_helper.rb"
|
49
|
+
]
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
56
|
+
else
|
57
|
+
end
|
58
|
+
else
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
# Set of basic array math functions.
|
4
|
+
module ArrayMath
|
5
|
+
|
6
|
+
def aaverage
|
7
|
+
accum = self.asum
|
8
|
+
return nil if accum.nil? || self.size == 0
|
9
|
+
accum.to_f / self.size
|
10
|
+
end
|
11
|
+
|
12
|
+
def asum
|
13
|
+
self.map {|i| return nil if i.is_a?(String)}
|
14
|
+
inject(0){ |sum,item| sum + item }
|
15
|
+
end
|
16
|
+
|
17
|
+
## Retun array FACTOR smaller. Average values to get smaller
|
18
|
+
def smoothed(factor)
|
19
|
+
self.enum_for(:each_slice, factor).map { |snipit| snipit.compact.aaverage }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Array
|
24
|
+
include ArrayMath
|
25
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Importer
|
2
|
+
class Garmin
|
3
|
+
def initialize(opts = {:data => nil})
|
4
|
+
@data = opts[:data]
|
5
|
+
end
|
6
|
+
|
7
|
+
def restore
|
8
|
+
workout = HRMParser::Workout.new(:duration => 0)
|
9
|
+
#data = Importer.read_in_file(@file_name)
|
10
|
+
|
11
|
+
@xml = Hpricot::XML(@data)
|
12
|
+
workout.time = Time.parse((@xml/:Id).innerHTML)
|
13
|
+
|
14
|
+
# Grab the duration from the lap. This _can_ be totally, completly wrong - AKA 10 years long.
|
15
|
+
# So if we have trackpoints, we'll replace it down lower
|
16
|
+
(@xml/:Lap).each do |lap|
|
17
|
+
f_time = (lap/:TotalTimeSeconds).innerHTML
|
18
|
+
workout.duration += Float f_time
|
19
|
+
end
|
20
|
+
|
21
|
+
found = false
|
22
|
+
trackpoints = Array.new
|
23
|
+
distance_one = nil
|
24
|
+
time_one = nil
|
25
|
+
|
26
|
+
totaldistance = 0
|
27
|
+
|
28
|
+
(@xml/:Trackpoint).each do |t|
|
29
|
+
found = true
|
30
|
+
trackpoint = HRMParser::TrackPoint.new
|
31
|
+
|
32
|
+
trackpoint.time = Time.parse((t/:Time).innerHTML)
|
33
|
+
|
34
|
+
hr = (t/:HeartRateBpm/:Value).innerHTML
|
35
|
+
alt = (t/:AltitudeMeters).innerHTML
|
36
|
+
dis = (t/:DistanceMeters).innerHTML
|
37
|
+
|
38
|
+
trackpoint.hr = hr != "" ? hr.to_i : nil
|
39
|
+
trackpoint.altitude = alt != "" ? alt.to_f : nil
|
40
|
+
trackpoint.distance = dis != "" ? dis.to_f : nil
|
41
|
+
|
42
|
+
(t/:Position).each do |p|
|
43
|
+
trackpoint.lat = (p/:LatitudeDegrees).innerHTML.to_f
|
44
|
+
trackpoint.lng = (p/:LongitudeDegrees).innerHTML.to_f
|
45
|
+
end
|
46
|
+
|
47
|
+
if trackpoint.distance.nil? && !trackpoint.lat.nil?
|
48
|
+
totaldistance += trackpoint.calc_distance(trackpoints.last, trackpoint)
|
49
|
+
trackpoint.distance = totaldistance
|
50
|
+
end
|
51
|
+
trackpoint.speed = trackpoint.calc_speed(trackpoints.last, trackpoint)
|
52
|
+
|
53
|
+
trackpoints << trackpoint
|
54
|
+
|
55
|
+
|
56
|
+
## CALCULATE SPEED. ICK.
|
57
|
+
# if distance_one.nil?
|
58
|
+
# distance_one = trackpoint.distance
|
59
|
+
# time_one = trackpoint.time
|
60
|
+
# else
|
61
|
+
# distance_two = trackpoint.distance
|
62
|
+
# next if distance_two.nil?
|
63
|
+
# time_two = trackpoint.time
|
64
|
+
# time_delta = time_two - time_one
|
65
|
+
# distance_delta = distance_two - distance_one
|
66
|
+
# if (distance_delta > 0 && time_delta > 0)
|
67
|
+
# trackpoint.speed = distance_delta / time_delta
|
68
|
+
# distance_one = distance_two
|
69
|
+
# time_one = time_two
|
70
|
+
# else
|
71
|
+
# trackpoint.speed = nil
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
end
|
75
|
+
|
76
|
+
if found
|
77
|
+
workout.duration = trackpoints.last.time - trackpoints.first.time
|
78
|
+
workout.trackpoints = trackpoints
|
79
|
+
workout.calc_average_speed!
|
80
|
+
workout.calc_altitude_gain!
|
81
|
+
workout.calc_average_hr!
|
82
|
+
workout.set_distance_from_trackpoints!
|
83
|
+
end
|
84
|
+
|
85
|
+
return workout
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Importer
|
2
|
+
class GPX
|
3
|
+
def initialize(opts = {:data => nil, :time_zone => "UTC"})
|
4
|
+
@data = opts[:data]
|
5
|
+
end
|
6
|
+
|
7
|
+
def restore
|
8
|
+
workout = HRMParser::Workout.new(:duration => 0)
|
9
|
+
@xml = Hpricot::XML(@data)
|
10
|
+
|
11
|
+
# Set the time based on first trackpoint. Seen an instance where the gpx begining time is wrong
|
12
|
+
ttime = (@xml/:trk/:trkpt/:time).first.innerHTML
|
13
|
+
workout.time = Time.parse(ttime)
|
14
|
+
|
15
|
+
trackpoints = []
|
16
|
+
distance = 0
|
17
|
+
workout.duration = 0
|
18
|
+
last_trackpoint = false
|
19
|
+
(@xml/:trk).each do |trk|
|
20
|
+
(trk/:trkpt).each do |trkpt|
|
21
|
+
trackpoint = HRMParser::TrackPoint.new
|
22
|
+
trackpoint.altitude = (trkpt/:ele).innerHTML.to_f
|
23
|
+
trackpoint.time = Time.parse((trkpt/:time).innerHTML)
|
24
|
+
|
25
|
+
trackpoint.lat = (trkpt.attributes)["lat"].to_f
|
26
|
+
trackpoint.lng = (trkpt.attributes)["lon"].to_f
|
27
|
+
|
28
|
+
distance += trackpoint.calc_distance(trackpoints.last, trackpoint)
|
29
|
+
trackpoint.distance = distance
|
30
|
+
|
31
|
+
trackpoint.speed = trackpoint.calc_speed(trackpoints.last, trackpoint)
|
32
|
+
|
33
|
+
if last_trackpoint && trackpoints.last && trackpoint.speed
|
34
|
+
workout.duration += trackpoint.time - last_trackpoint.time if trackpoint.speed > 0.04
|
35
|
+
end
|
36
|
+
trackpoints << trackpoint
|
37
|
+
last_trackpoint = trackpoint
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# workout.duration = trackpoints.last.time - trackpoints.first.time
|
43
|
+
workout.trackpoints = trackpoints
|
44
|
+
workout.calc_average_speed!
|
45
|
+
workout.calc_altitude_gain!
|
46
|
+
workout.distance = trackpoints.last.distance
|
47
|
+
return workout
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Importer
|
2
|
+
class Polar
|
3
|
+
|
4
|
+
attr_reader :time_zone
|
5
|
+
|
6
|
+
def initialize(opts = {:data => nil, :time_zone => "UTC"})
|
7
|
+
@data = opts[:data]
|
8
|
+
@time_zone = opts[:time_zone]
|
9
|
+
end
|
10
|
+
|
11
|
+
def restore
|
12
|
+
workout = HRMParser::Workout.new(:duration => 0)
|
13
|
+
|
14
|
+
params = parse_params
|
15
|
+
|
16
|
+
date = params["Date"] + " " + params["StartTime"] + " " + @time_zone
|
17
|
+
|
18
|
+
length_array = params["Length"].split(/:/)
|
19
|
+
workout.duration = (length_array[0].to_f * 3600) + (length_array[1].to_f * 60) + (length_array[2].to_f)
|
20
|
+
workout.time = Time.parse(date)
|
21
|
+
|
22
|
+
workout.trackpoints = get_trackpoints(workout.time, params["Interval"].to_i)
|
23
|
+
|
24
|
+
workout.calc_average_hr!
|
25
|
+
|
26
|
+
return workout
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# def parse_tabbed_blocks
|
34
|
+
# # This is the list of tabbed blocks
|
35
|
+
# tabbed_blocks = %w[IntTimes ExtraData Sumary-123 Summary-TH]
|
36
|
+
# tabbed_blocks.each do |block_name|
|
37
|
+
# @polarHash[block_name] = []
|
38
|
+
# block_text = find_block(block_name)
|
39
|
+
# block_text.each do |block_line|
|
40
|
+
# @polarHash[block_name] << block_line.split(/\t/)
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
|
45
|
+
# Params is the only "ini" style block
|
46
|
+
def parse_params
|
47
|
+
hash = {}
|
48
|
+
param_block = find_block("Params")
|
49
|
+
param_block.each do |param|
|
50
|
+
# /=/ in case that doesn't work
|
51
|
+
key, value = param.split("=", 2)
|
52
|
+
key = key.strip unless key.nil?
|
53
|
+
value = value.strip unless value.nil?
|
54
|
+
hash[key] = value unless key.nil?
|
55
|
+
end
|
56
|
+
return hash
|
57
|
+
end
|
58
|
+
|
59
|
+
# Polar file has [Foo] blocks. Return the data in the block
|
60
|
+
def find_block(header)
|
61
|
+
found = false
|
62
|
+
block = []
|
63
|
+
@data.each do |line|
|
64
|
+
line.chomp!
|
65
|
+
found = false if line =~ /^\[.*\]$/
|
66
|
+
block << line if found
|
67
|
+
found = true if line =~ /\[#{header}\]/
|
68
|
+
end
|
69
|
+
return block
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_hrdata
|
73
|
+
hrdata = []
|
74
|
+
block_text = find_block("HRData")
|
75
|
+
block_text.each do |block_line|
|
76
|
+
hrdata << block_line.chomp
|
77
|
+
end
|
78
|
+
return hrdata
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_trackpoints(start_time, interval)
|
82
|
+
trackpoints = []
|
83
|
+
rrcounter = 0
|
84
|
+
|
85
|
+
hrdata = parse_hrdata
|
86
|
+
hrdata.each do |hrd|
|
87
|
+
tp = HRMParser::TrackPoint.new
|
88
|
+
|
89
|
+
if (interval == 238)
|
90
|
+
rrcounter += hrd.to_i
|
91
|
+
tp.hr = 60000 / hrd.to_i
|
92
|
+
tp.time = start_time + (rrcounter/1000)
|
93
|
+
else
|
94
|
+
tp.hr = hrd.to_i
|
95
|
+
tp.time = start_time + (interval * trackpoints.size)
|
96
|
+
end
|
97
|
+
|
98
|
+
trackpoints << tp
|
99
|
+
end
|
100
|
+
return trackpoints
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Importer
|
2
|
+
class Suunto
|
3
|
+
attr_reader :time_zone
|
4
|
+
|
5
|
+
def initialize(opts = {:data => nil, :time_zone => "UTC"})
|
6
|
+
@data = opts[:data]
|
7
|
+
@time_zone = opts[:time_zone]
|
8
|
+
end
|
9
|
+
|
10
|
+
def restore
|
11
|
+
workout = HRMParser::Workout.new(:duration => 0)
|
12
|
+
|
13
|
+
params = parse_params("HEADER")
|
14
|
+
|
15
|
+
# Using DateTime because 1.8 at leas doesn't have a Time.strptime
|
16
|
+
# And european ordeirng consfuses time.parse
|
17
|
+
# TODO: must be some better way
|
18
|
+
dt = DateTime.strptime(params["STARTTIME"], "%d.%m.%Y %H:%M.%S")
|
19
|
+
time_for_parse = dt.strftime("%b %d %H:%M:%S @time_zone %Y")
|
20
|
+
|
21
|
+
workout.time = Time.parse(time_for_parse)
|
22
|
+
workout.duration = params["DURATION"].to_f
|
23
|
+
|
24
|
+
workout.trackpoints = get_trackpoints
|
25
|
+
|
26
|
+
workout.calc_average_hr!
|
27
|
+
workout.calc_altitude_gain!
|
28
|
+
workout.calc_average_speed!
|
29
|
+
workout.set_distance_from_trackpoints!
|
30
|
+
|
31
|
+
return workout
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse_params(string)
|
37
|
+
hash = {}
|
38
|
+
param_block = find_block(string)
|
39
|
+
param_block.each do |param|
|
40
|
+
# /=/ in case that doesn't work
|
41
|
+
key, value = param.split("=", 2)
|
42
|
+
key = key.strip unless key.nil?
|
43
|
+
value = value.strip unless value.nil?
|
44
|
+
hash[key] = value unless key.nil?
|
45
|
+
end
|
46
|
+
return hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_block(header)
|
50
|
+
found = false
|
51
|
+
block = []
|
52
|
+
@data.each do |line|
|
53
|
+
line.chomp!
|
54
|
+
found = false if line =~ /^\[.*\]$/
|
55
|
+
block << line if found
|
56
|
+
found = true if line =~ /\[#{header}\]/
|
57
|
+
end
|
58
|
+
return block
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_data(string)
|
62
|
+
data = []
|
63
|
+
block_text = find_block(string)
|
64
|
+
block_text.each do |block_line|
|
65
|
+
data << block_line.chomp
|
66
|
+
end
|
67
|
+
return data
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_trackpoints
|
71
|
+
trackpoints = []
|
72
|
+
logs = parse_data("POINTS")
|
73
|
+
for line in logs do
|
74
|
+
type, date, time, altitude, blank, blank, hr, epoc, respiration, ventilation, vo2, kcal, blank, blank, distance, speed, cadence, temp = line.split(/,/)
|
75
|
+
next if type == "\"T6LAP\""
|
76
|
+
|
77
|
+
trackpoint = HRMParser::TrackPoint.new
|
78
|
+
|
79
|
+
points_f = %w[epoc kcal speed]
|
80
|
+
points_i = %w[altitude hr respiration ventilation vo2 distance cadence temp]
|
81
|
+
|
82
|
+
points_f.each do |p|
|
83
|
+
value = (eval p).to_f
|
84
|
+
value = nil if value == 0.0
|
85
|
+
trackpoint.send("#{p}=".to_sym, value)
|
86
|
+
end
|
87
|
+
|
88
|
+
points_i.each do |p|
|
89
|
+
value = (eval p).to_i
|
90
|
+
value = nil if value == 0
|
91
|
+
trackpoint.send("#{p}=".to_sym, value)
|
92
|
+
end
|
93
|
+
dt = DateTime.strptime(date + " " + time, "%d.%m.%Y %H:%M.%S")
|
94
|
+
time_for_parse = dt.strftime("%b %d %H:%M:%S @time_zone %Y")
|
95
|
+
trackpoint.time = Time.parse(time_for_parse)
|
96
|
+
|
97
|
+
trackpoints << trackpoint
|
98
|
+
end
|
99
|
+
return trackpoints
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Importer
|
2
|
+
class Timex
|
3
|
+
attr_reader :time_zone
|
4
|
+
|
5
|
+
def initialize(opts = {:data => nil, :time_zone => "UTC"})
|
6
|
+
@data = opts[:data]
|
7
|
+
@time_zone = opts[:time_zone]
|
8
|
+
end
|
9
|
+
|
10
|
+
def restore
|
11
|
+
workout = HRMParser::Workout.new(:duration => 0)
|
12
|
+
|
13
|
+
params = parse_params("session data")
|
14
|
+
|
15
|
+
dt = DateTime.strptime(params["Sessiondate"], "%d/%m/%Y %H:%M:%S")
|
16
|
+
time_for_parse = dt.strftime("%b %d %H:%M:%S @time_zone %Y")
|
17
|
+
|
18
|
+
workout.time = Time.parse(time_for_parse)
|
19
|
+
workout.duration = params["duration"].to_f
|
20
|
+
|
21
|
+
workout.trackpoints = get_trackpoints(workout.time)
|
22
|
+
|
23
|
+
workout.calc_average_hr!
|
24
|
+
workout.calc_altitude_gain!
|
25
|
+
workout.calc_average_speed!
|
26
|
+
workout.set_distance_from_trackpoints!
|
27
|
+
|
28
|
+
return workout
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parse_params(string)
|
36
|
+
hash = {}
|
37
|
+
param_block = find_block(string)
|
38
|
+
param_block.each do |param|
|
39
|
+
# /=/ in case that doesn't work
|
40
|
+
key, value = param.split("=", 2)
|
41
|
+
key = key.strip unless key.nil?
|
42
|
+
value = value.strip unless value.nil?
|
43
|
+
hash[key] = value unless key.nil?
|
44
|
+
end
|
45
|
+
return hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_block(header)
|
49
|
+
found = false
|
50
|
+
block = []
|
51
|
+
@data.each do |line|
|
52
|
+
line.chomp!
|
53
|
+
found = false if line =~ /^\[.*\]$/
|
54
|
+
block << line if found
|
55
|
+
found = true if line =~ /\[#{header}\]/
|
56
|
+
end
|
57
|
+
return block
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_data(string)
|
61
|
+
data = []
|
62
|
+
block_text = find_block(string)
|
63
|
+
block_text.each do |block_line|
|
64
|
+
data << block_line.chomp
|
65
|
+
end
|
66
|
+
return data
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_trackpoints(base_time)
|
70
|
+
trackpoints = []
|
71
|
+
have_gps = false
|
72
|
+
logs = parse_data("recorded")
|
73
|
+
fields = logs[0].split(/,/)
|
74
|
+
have_gps = true if fields.size > 5
|
75
|
+
|
76
|
+
for line in logs do
|
77
|
+
if have_gps
|
78
|
+
if line =~ /^".*"/ then line.gsub!(/"(.*?)"/,'\1') end # remove double-quotes at string beginning & end
|
79
|
+
seconds, hr, speed_imperial, distance_imperial, data_flag, lng, lat, altitude, acqs, trueh, magh = line.split(/,/)
|
80
|
+
else
|
81
|
+
seconds, hr, speed_imperial, distance_imperial, data_flag = line.split(/,/)
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
## Convert speed and distance to metric - meters specifically
|
86
|
+
distance = distance_imperial.to_f * 1609.344
|
87
|
+
speed = speed_imperial.to_f * 0.44704
|
88
|
+
|
89
|
+
|
90
|
+
trackpoint = HRMParser::TrackPoint.new
|
91
|
+
|
92
|
+
points_f = %w[speed distance lat lng altitude]
|
93
|
+
points_i = %w[hr]
|
94
|
+
|
95
|
+
|
96
|
+
points_f.each do |p|
|
97
|
+
value = (eval p).to_f
|
98
|
+
value = nil if value == 0.0
|
99
|
+
trackpoint.send("#{p}=".to_sym, value)
|
100
|
+
end
|
101
|
+
|
102
|
+
points_i.each do |p|
|
103
|
+
value = (eval p).to_i
|
104
|
+
value = nil if value == 0
|
105
|
+
trackpoint.send("#{p}=".to_sym, value)
|
106
|
+
end
|
107
|
+
|
108
|
+
trackpoint.time = base_time + seconds.to_i
|
109
|
+
|
110
|
+
trackpoints << trackpoint
|
111
|
+
end
|
112
|
+
return trackpoints
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'importer/garmin'
|
2
|
+
require 'importer/polar'
|
3
|
+
require 'importer/suunto'
|
4
|
+
require 'importer/gpx'
|
5
|
+
require 'importer/timex'
|
6
|
+
|
7
|
+
module Importer
|
8
|
+
def Importer.file_type(name)
|
9
|
+
case name
|
10
|
+
when /\.tcx$/i
|
11
|
+
return "GARMIN_XML"
|
12
|
+
when /\.hrm$/i
|
13
|
+
return "POLAR_HRM"
|
14
|
+
when /\.sdf$/i
|
15
|
+
return "SUUNTO"
|
16
|
+
when /\.gpx$/i
|
17
|
+
return "GPX"
|
18
|
+
when /\.csv$/i
|
19
|
+
f = File.new(name)
|
20
|
+
first_line = f.readline
|
21
|
+
f.close
|
22
|
+
if first_line.chomp == "[Timex Trainer Data File]"
|
23
|
+
return "TIMEX"
|
24
|
+
else
|
25
|
+
return "UNKNOWN CSV"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def Importer.read_in_file(name)
|
31
|
+
if File.readable?(name)
|
32
|
+
return open(name, "r")
|
33
|
+
else
|
34
|
+
puts "FILE ERROR, can't read #{name}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module HRMParser
|
2
|
+
class TrackPoint
|
3
|
+
|
4
|
+
RAD_PER_DEG = 0.017453293 # PI/180
|
5
|
+
|
6
|
+
attr_accessor :lat, :lng, :altitude, :speed, :hr, :distance, :time, :cadence, :temp, :kcal, :epoc, :respiration, :ventilation, :vo2, :cadence
|
7
|
+
|
8
|
+
def initialize(opts = {:lat => nil, :lng => nil, :altitude => nil, :speed => nil, :hr => nil, :distance => nil, :cadence => nil, :time => Time.now})
|
9
|
+
@lat = opts[:lat]
|
10
|
+
@lng = opts[:lng]
|
11
|
+
@altitude = opts[:altitude]
|
12
|
+
@speed = opts[:speed]
|
13
|
+
@hr = opts[:hr]
|
14
|
+
@distance = opts[:distance]
|
15
|
+
@time = opts[:time]
|
16
|
+
@cadence = opts[:cadence]
|
17
|
+
end
|
18
|
+
|
19
|
+
def calc_distance(pointA, pointB)
|
20
|
+
return 0 if pointA.nil? || pointA.lat.nil? || pointB.nil? || pointB.lat.nil?
|
21
|
+
|
22
|
+
dlng = pointB.lng - pointA.lng
|
23
|
+
dlat = pointB.lat - pointA.lat
|
24
|
+
|
25
|
+
dlat_rad = dlat * RAD_PER_DEG
|
26
|
+
dlng_rad = dlng * RAD_PER_DEG
|
27
|
+
|
28
|
+
lat1_rad = pointA.lat * RAD_PER_DEG
|
29
|
+
lng1_rad = pointA.lng * RAD_PER_DEG
|
30
|
+
|
31
|
+
lat2_rad = pointB.lat * RAD_PER_DEG
|
32
|
+
lng2_rad = pointB.lng * RAD_PER_DEG
|
33
|
+
|
34
|
+
a = (Math.sin(dlat_rad/2))**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * (Math.sin(dlng_rad/2))**2
|
35
|
+
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
|
36
|
+
|
37
|
+
return 6371000 * c
|
38
|
+
end
|
39
|
+
|
40
|
+
def calc_speed(pointA, pointB)
|
41
|
+
return nil if pointA.nil? || pointA.lat.nil? || pointB.nil? || pointB.lat.nil?
|
42
|
+
time_delta = pointB.time - pointA.time
|
43
|
+
return nil if time_delta == 0
|
44
|
+
distance_delta = pointB.distance - pointA.distance
|
45
|
+
return distance_delta / time_delta
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module HRMParser
|
2
|
+
class Workout
|
3
|
+
attr_accessor :duration, :distance, :time, :name, :file_name, :trackpoints
|
4
|
+
attr_reader :average_hr, :data, :average_speed, :altitude_gain
|
5
|
+
|
6
|
+
def initialize(opts = {:duration => nil, :distance => nil, :time => Time.now, :name => nil, :file_name => nil})
|
7
|
+
@duration = opts[:duration]
|
8
|
+
@name = opts[:name]
|
9
|
+
@time = opts[:time]
|
10
|
+
@distance = opts[:distance]
|
11
|
+
@file_name = opts[:file_name]
|
12
|
+
|
13
|
+
@data = nil
|
14
|
+
@trackpoints = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def calc_average_hr!
|
19
|
+
ahr = heart_rates.compact.aaverage
|
20
|
+
@average_hr = ahr == 0.0 ? nil : ahr
|
21
|
+
end
|
22
|
+
|
23
|
+
def calc_average_speed!
|
24
|
+
aspeed = speeds.compact.aaverage
|
25
|
+
@average_speed = aspeed == 0.0 ? nil : aspeed
|
26
|
+
end
|
27
|
+
|
28
|
+
def calc_altitude_gain!
|
29
|
+
gain = 0
|
30
|
+
smoothed_altitude = altitudes.compact.smoothed(10)
|
31
|
+
start = smoothed_altitude.first
|
32
|
+
smoothed_altitude.each do |alt|
|
33
|
+
diff = alt - start
|
34
|
+
if (diff > 0.5)
|
35
|
+
gain += diff
|
36
|
+
end
|
37
|
+
start = alt
|
38
|
+
end
|
39
|
+
@altitude_gain = gain == 0.0 ? nil : gain
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
## Some helper functions that return specific files from trackpoint as array
|
45
|
+
def heart_rates
|
46
|
+
@trackpoints.map {|tp| tp.hr }
|
47
|
+
end
|
48
|
+
|
49
|
+
def speeds
|
50
|
+
@trackpoints.map {|tp| tp.speed }
|
51
|
+
end
|
52
|
+
|
53
|
+
def altitudes
|
54
|
+
@trackpoints.map { |tp| tp.altitude }
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_distance_from_trackpoints!
|
58
|
+
@trackpoints.reverse_each do |tp|
|
59
|
+
if !tp.distance.nil?
|
60
|
+
self.distance = tp.distance
|
61
|
+
break
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/hrmparser.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
module ArrayMath
|
4
|
+
describe "ArrayMath" do
|
5
|
+
context "Sum" do
|
6
|
+
it "sums an array" do
|
7
|
+
array = [1,2,3,4,5]
|
8
|
+
array.asum.should == 15
|
9
|
+
end
|
10
|
+
it "can not sum an array of strings" do
|
11
|
+
array = ["hello", "goodbye"]
|
12
|
+
array.asum.should == nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
context "average" do
|
16
|
+
it "averages an array of integers and returns a float" do
|
17
|
+
array = [2,3,4,3,2]
|
18
|
+
array.aaverage.should == 2.8
|
19
|
+
end
|
20
|
+
it "averages an array of floats" do
|
21
|
+
array = [1.5,2.5,3.5,2.5,1.5]
|
22
|
+
array.aaverage.should == 2.3
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
# Time to add your specs!
|
4
|
+
# http://rspec.info/
|
5
|
+
module HRMParser
|
6
|
+
describe "TrackPoint" do
|
7
|
+
context "new trackpoint" do
|
8
|
+
it "has no variables set" do
|
9
|
+
hrm = TrackPoint.new
|
10
|
+
hrm.speed == nil
|
11
|
+
hrm.distance == nil
|
12
|
+
hrm.lat.should == nil
|
13
|
+
hrm.lng.should == nil
|
14
|
+
hrm.altitude.should == nil
|
15
|
+
hrm.hr.should == nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "Workout" do
|
21
|
+
context "new workout" do
|
22
|
+
it "has no variables set" do
|
23
|
+
workout = Workout.new
|
24
|
+
workout.distance.should == nil
|
25
|
+
workout.duration.should == nil
|
26
|
+
workout.average_hr.should == nil
|
27
|
+
workout.name.should == nil
|
28
|
+
workout.file_name.should == nil
|
29
|
+
end
|
30
|
+
it "set name through initializer" do
|
31
|
+
workout = Workout.new(:name => "test workout")
|
32
|
+
workout.name.should == "test workout"
|
33
|
+
end
|
34
|
+
it "can not set average_hr during init" do
|
35
|
+
workout = Workout.new(:average_hr => 150)
|
36
|
+
workout.average_hr.should == nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "Identifies files" do
|
41
|
+
it "identify file as garmin" do
|
42
|
+
type = Importer.file_type("spec/samples/small-garmin.TCX")
|
43
|
+
type.should == "GARMIN_XML"
|
44
|
+
end
|
45
|
+
it "identification returns nil if no file specified" do
|
46
|
+
type = Importer.file_type("")
|
47
|
+
type.should == nil
|
48
|
+
end
|
49
|
+
it "identify file as polar" do
|
50
|
+
type = Importer.file_type("spec/samples/polarRS400.hrm")
|
51
|
+
type.should == "POLAR_HRM"
|
52
|
+
end
|
53
|
+
it "identifies timex" do
|
54
|
+
type = Importer.file_type("spec/samples/timex/HR.csv")
|
55
|
+
type.should == "TIMEX"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# context "Parse a GPS file" do
|
60
|
+
# it "finds the duration, time" do
|
61
|
+
# filename = "spec/samples/gps-with-suunto.gpx"
|
62
|
+
# data = File.read(filename)
|
63
|
+
# importer = Importer::GPX.new(:data => data)
|
64
|
+
# workout = importer.restore
|
65
|
+
# workout.time.should == Time.parse("Thu May 07 21:32:31 UTC 2009")
|
66
|
+
#
|
67
|
+
# # Duration is actualy less, but we don't account for stopped time right now
|
68
|
+
# workout.duration.should be_close(6284,1)
|
69
|
+
# end
|
70
|
+
# it "calculates the distance and speed" do
|
71
|
+
# filename = "spec/samples/gps-with-suunto.gpx"
|
72
|
+
# data = File.read(filename)
|
73
|
+
# importer = Importer::GPX.new(:data => data)
|
74
|
+
# workout = importer.restore
|
75
|
+
# workout.average_speed.should be_close(6.7, 0.2)
|
76
|
+
# workout.distance.should be_close(26427, 1)
|
77
|
+
# end
|
78
|
+
# it "handles files with drops" do
|
79
|
+
# filename = "spec/samples/gps-flat-run.gpx"
|
80
|
+
# data = File.read(filename)
|
81
|
+
# importer = Importer::GPX.new(:data => data)
|
82
|
+
# workout = importer.restore
|
83
|
+
# workout.average_speed.should be_close(2.9, 0.2)
|
84
|
+
# workout.distance.should be_close(11453, 1)
|
85
|
+
# workout.altitude_gain.should be_close(325, 10)
|
86
|
+
# end
|
87
|
+
# it "deals with stopping and calculates duration correctly" do
|
88
|
+
# filename = "spec/samples/gps-with-stops.gpx"
|
89
|
+
# data = File.read(filename)
|
90
|
+
# importer = Importer::GPX.new(:data => data)
|
91
|
+
# workout = importer.restore
|
92
|
+
# workout.average_speed.should be_close(6.5, 0.2)
|
93
|
+
# workout.distance.should be_close(5230, 1)
|
94
|
+
# workout.altitude_gain.should be_close(11, 10)
|
95
|
+
# workout.duration.should be_close(1149, 1)
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# it "deals with stopping and calculates duration correctly" do
|
99
|
+
# filename = "spec/samples/gps-with-stops-2.gpx"
|
100
|
+
# data = File.read(filename)
|
101
|
+
# importer = Importer::GPX.new(:data => data)
|
102
|
+
# workout = importer.restore
|
103
|
+
# workout.average_speed.should be_close(5.7, 0.2)
|
104
|
+
# workout.distance.should be_close(3099, 1)
|
105
|
+
# workout.altitude_gain.should be_close(24, 10)
|
106
|
+
# workout.duration.should be_close(564, 1)
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# context "Parse garmin file" do
|
112
|
+
# it "finds workout start time on a short workout" do
|
113
|
+
# filename = "spec/samples/indoor-garmin-405.TCX"
|
114
|
+
# data = File.read(filename)
|
115
|
+
# importer = Importer::Garmin.new(:data => data)
|
116
|
+
# workout = importer.restore
|
117
|
+
# workout.time.should == Time.parse("Fri Aug 22 01:04:55 UTC 2008")
|
118
|
+
# end
|
119
|
+
# it "finds the duration on a short workout" do
|
120
|
+
# filename = "spec/samples/indoor-garmin-405.TCX"
|
121
|
+
# data = File.read(filename)
|
122
|
+
# importer = Importer::Garmin.new(:data => data)
|
123
|
+
# workout = importer.restore
|
124
|
+
# workout.duration.should be_close(755, 1)
|
125
|
+
# end
|
126
|
+
# it "indoor workout has no trackpoints" do
|
127
|
+
# filename = "spec/samples/indoor-garmin-405.TCX"
|
128
|
+
# data = File.read(filename)
|
129
|
+
# importer = Importer::Garmin.new(:data => data)
|
130
|
+
# workout = importer.restore
|
131
|
+
# workout.distance.should be_nil
|
132
|
+
# workout.average_hr.should be_nil
|
133
|
+
# workout.average_speed.should be_nil
|
134
|
+
# workout.altitude_gain.should be_nil
|
135
|
+
# workout.trackpoints.should == {}
|
136
|
+
# end
|
137
|
+
# it "parses files with only LAT and LNG" do
|
138
|
+
# filename = "spec/samples/garmin-only-lat-lng.tcx"
|
139
|
+
# data = File.read(filename)
|
140
|
+
# importer = Importer::Garmin.new(:data => data)
|
141
|
+
# workout = importer.restore
|
142
|
+
# workout.distance.should be_close(172052, 1)
|
143
|
+
# workout.average_hr.should be_nil
|
144
|
+
# workout.average_speed.should be_close(5.93, 0.1)
|
145
|
+
# workout.altitude_gain.should be_close(372, 10)
|
146
|
+
# end
|
147
|
+
#
|
148
|
+
# # Parsing the full XML is just slow. Commenting out for now.
|
149
|
+
# it "gets workout level settings for outdoor workout" do
|
150
|
+
# filename = "spec/samples/outdoor-garmin-405.TCX"
|
151
|
+
# data = File.read(filename)
|
152
|
+
# importer = Importer::Garmin.new(:data => data)
|
153
|
+
# workout = importer.restore
|
154
|
+
# workout.distance.should be_close(11740, 5)
|
155
|
+
# workout.average_hr.should be_close(149.7, 0.5)
|
156
|
+
# workout.average_speed.should be_close(1.5, 0.2)
|
157
|
+
# workout.altitude_gain.should be_close(580, 25)
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# it "gets workout level settings for weird distance workout" do
|
161
|
+
# filename = "spec/samples/garmin-405-dies-distance.TCX"
|
162
|
+
# data = File.read(filename)
|
163
|
+
# importer = Importer::Garmin.new(:data => data)
|
164
|
+
# workout = importer.restore
|
165
|
+
# workout.distance.should be_close(9426, 1)
|
166
|
+
# workout.average_hr.should == nil
|
167
|
+
# workout.average_speed.should be_close(6.7, 0.2)
|
168
|
+
# workout.altitude_gain.should be_close(40, 10)
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# it "doesn't have any 0 in latitude" do
|
172
|
+
# filename = "spec/samples/garmin-405-with-0-0.TCX"
|
173
|
+
# data = File.read(filename)
|
174
|
+
# importer = Importer::Garmin.new(:data => data)
|
175
|
+
# workout = importer.restore
|
176
|
+
# workout.trackpoints.map {|tp| tp.lat.should_not == 0.0}
|
177
|
+
# workout.trackpoints.map {|tp| tp.lat.should_not == "undefined"}
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# it "handles files with INSANE duration" do
|
181
|
+
# filename = "spec/samples/insane-duration.TCX"
|
182
|
+
# data = File.read(filename)
|
183
|
+
# importer = Importer::Garmin.new(:data => data)
|
184
|
+
# workout = importer.restore
|
185
|
+
# workout.duration.should be_close(4996, 0.2)
|
186
|
+
# end
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# context "Parse polar RS400 file" do
|
190
|
+
# it "finds the duration and time" do
|
191
|
+
# filename ="spec/samples/polarRS400.hrm"
|
192
|
+
# data = File.read(filename)
|
193
|
+
# importer = Importer::Polar.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
194
|
+
# workout = importer.restore
|
195
|
+
# workout.duration.should be_close(3569,1)
|
196
|
+
# workout.time.should == Time.parse("Thu Apr 16 12:01:55 -0700 2009")
|
197
|
+
# end
|
198
|
+
# it "calculates the average heartrate" do
|
199
|
+
# filename ="spec/samples/polarRS400.hrm"
|
200
|
+
# data = File.read(filename)
|
201
|
+
# importer = Importer::Polar.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
202
|
+
# workout = importer.restore
|
203
|
+
# workout.average_hr.should be_close(145, 1)
|
204
|
+
# end
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# context "Parse a Polar RR file" do
|
208
|
+
# it "calculates the heart rate from RR" do
|
209
|
+
# filename ="spec/samples/polarRS800-RR.hrm"
|
210
|
+
# data = File.read(filename)
|
211
|
+
# importer = Importer::Polar.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
212
|
+
# workout = importer.restore
|
213
|
+
# workout.trackpoints.each {|tp| tp.hr.should < 220 && tp.hr.should > 30}
|
214
|
+
# workout.average_hr.should be_close(115, 1)
|
215
|
+
# workout.average_speed.should == nil
|
216
|
+
# end
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# context "Parse a Suunto T6C RR file" do
|
220
|
+
# it "finds the duration and time" do
|
221
|
+
# filename = "spec/samples/suunto-t6-RR-stops.sdf"
|
222
|
+
# data = File.read(filename)
|
223
|
+
# importer = Importer::Suunto.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
224
|
+
# workout = importer.restore
|
225
|
+
# workout.duration.should be_close(4781,1)
|
226
|
+
# workout.time.should == Time.parse("Thu May 07 14:16:07 -0700 2009")
|
227
|
+
# end
|
228
|
+
# it "calculates the average HR & altitude" do
|
229
|
+
# filename = "spec/samples/suunto-t6-RR-stops.sdf"
|
230
|
+
# data = File.read(filename)
|
231
|
+
# importer = Importer::Suunto.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
232
|
+
# workout = importer.restore
|
233
|
+
# workout.average_hr.should be_close(152,1)
|
234
|
+
# workout.average_speed.should == nil
|
235
|
+
# workout.trackpoints.each { |tp| tp.speed.should == nil }
|
236
|
+
# workout.altitude_gain.should be_close(115, 10)
|
237
|
+
# end
|
238
|
+
# it "calculates the speed and distance" do
|
239
|
+
# filename = "spec/samples/suunto-with-cadence-speed-distance.sdf"
|
240
|
+
# data = File.read(filename)
|
241
|
+
# importer = Importer::Suunto.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
242
|
+
# workout = importer.restore
|
243
|
+
# workout.average_hr.should be_close(131,1)
|
244
|
+
# workout.average_speed.should be_close(9.3, 0.1)
|
245
|
+
# workout.altitude_gain.should be_close(75, 15)
|
246
|
+
# workout.distance.should == 124597
|
247
|
+
# end
|
248
|
+
# end
|
249
|
+
|
250
|
+
context "Parse Timex CSV" do
|
251
|
+
it "handles HR only" do
|
252
|
+
filename = "spec/samples/timex/HR.csv"
|
253
|
+
data = File.read(filename)
|
254
|
+
importer = Importer::Timex.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
255
|
+
workout = importer.restore
|
256
|
+
workout.duration.should be_close(3298, 1)
|
257
|
+
workout.time.should == Time.parse("Jan 01 09:27:26 -0800 2009")
|
258
|
+
workout.average_hr.should be_close(81,1)
|
259
|
+
end
|
260
|
+
|
261
|
+
it "does speed and distance" do
|
262
|
+
filename = "spec/samples/timex/Speed+Distance.csv"
|
263
|
+
data = File.read(filename)
|
264
|
+
importer = Importer::Timex.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
265
|
+
workout = importer.restore
|
266
|
+
workout.duration.should be_close(2580, 1)
|
267
|
+
workout.time.should == Time.parse("Feb 01 08:27:02 -0800 2009")
|
268
|
+
workout.average_hr.should == nil
|
269
|
+
workout.distance.should be_close(8391, 1)
|
270
|
+
workout.average_speed.should be_close(3.2, 0.2)
|
271
|
+
end
|
272
|
+
|
273
|
+
it "handles a GPS only file" do
|
274
|
+
filename = "spec/samples/timex/GPS.csv"
|
275
|
+
data = File.read(filename)
|
276
|
+
importer = Importer::Timex.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
277
|
+
workout = importer.restore
|
278
|
+
workout.duration.should be_close(1937, 1)
|
279
|
+
workout.time.should == Time.parse("Jun 01 12:03:12 -0700 2009")
|
280
|
+
workout.average_hr.should == nil
|
281
|
+
workout.distance.should be_close(6035, 1)
|
282
|
+
workout.average_speed.should be_close(3.05, 0.05)
|
283
|
+
end
|
284
|
+
|
285
|
+
it "handles GPS & HR" do
|
286
|
+
filename = "spec/samples/timex/GPS+HR.csv"
|
287
|
+
data = File.read(filename)
|
288
|
+
importer = Importer::Timex.new(:data => data, :time_zone => "Pacific Time (US & Canada)")
|
289
|
+
workout = importer.restore
|
290
|
+
workout.duration.should be_close(2677.0, 1)
|
291
|
+
workout.time.should == Time.parse("Jul 01 12:00:43 -0700 2009")
|
292
|
+
workout.average_hr.should be_close(169,1)
|
293
|
+
workout.distance.should be_close(6435, 1)
|
294
|
+
workout.average_speed.should be_close(2.34, 0.05)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
lib_path = File.expand_path("#{dir}/../lib")
|
3
|
+
$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
|
4
|
+
$_spec_spec = true # Prevents Kernel.exit in various places
|
5
|
+
|
6
|
+
require 'spec'
|
7
|
+
|
8
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
9
|
+
require 'hrmparser'
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hrmparser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oren Teich
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-13 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Parses Polar and Garmin HRM files.
|
17
|
+
email: oren@teich.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- CHANGELOG.txt
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- hrmparser.gemspec
|
31
|
+
- lib/hrmparser.rb
|
32
|
+
- lib/hrmparser/arraymath.rb
|
33
|
+
- lib/hrmparser/importer.rb
|
34
|
+
- lib/hrmparser/importer/garmin.rb
|
35
|
+
- lib/hrmparser/importer/gpx.rb
|
36
|
+
- lib/hrmparser/importer/polar.rb
|
37
|
+
- lib/hrmparser/importer/suunto.rb
|
38
|
+
- lib/hrmparser/importer/timex.rb
|
39
|
+
- lib/hrmparser/trackpoint.rb
|
40
|
+
- lib/hrmparser/workout.rb
|
41
|
+
- spec/arraymath_spec.rb
|
42
|
+
- spec/hrmparser_spec.rb
|
43
|
+
- spec/spec.opts
|
44
|
+
- spec/spec_helper.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/teich/hrmparser
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Heart Rate Monitor Parser
|
73
|
+
test_files:
|
74
|
+
- spec/arraymath_spec.rb
|
75
|
+
- spec/hrmparser_spec.rb
|
76
|
+
- spec/spec_helper.rb
|