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