teich-hrmparser 0.4.0 → 0.4.1
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/CHANGELOG.txt +5 -0
- data/VERSION.yml +1 -1
- data/lib/hrmparser/importer/gpx.rb +42 -0
- data/lib/hrmparser/importer.rb +3 -0
- data/lib/hrmparser/trackpoint.rb +47 -14
- data/spec/hrmparser_spec.rb +118 -97
- metadata +3 -2
data/CHANGELOG.txt
CHANGED
data/VERSION.yml
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
ttime = (@xml/:time).first.innerHTML
|
|
11
|
+
workout.time = Time.parse(ttime)
|
|
12
|
+
|
|
13
|
+
trackpoints = []
|
|
14
|
+
distance = 0
|
|
15
|
+
(@xml/:trk).each do |trk|
|
|
16
|
+
(trk/:trkpt).each do |trkpt|
|
|
17
|
+
trackpoint = HRMParser::TrackPoint.new
|
|
18
|
+
trackpoint.altitude = (trkpt/:ele).innerHTML.to_f
|
|
19
|
+
trackpoint.time = Time.parse((trkpt/:time).innerHTML)
|
|
20
|
+
|
|
21
|
+
trackpoint.lat = (trkpt.attributes)["lat"].to_f
|
|
22
|
+
trackpoint.lng = (trkpt.attributes)["lon"].to_f
|
|
23
|
+
|
|
24
|
+
distance += trackpoint.calc_distance(trackpoints.last, trackpoint)
|
|
25
|
+
trackpoint.distance = distance
|
|
26
|
+
|
|
27
|
+
trackpoint.speed = trackpoint.calc_speed(trackpoints.last, trackpoint)
|
|
28
|
+
|
|
29
|
+
trackpoints << trackpoint
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
workout.duration = trackpoints.last.time - trackpoints.first.time
|
|
34
|
+
workout.trackpoints = trackpoints
|
|
35
|
+
workout.calc_average_speed!
|
|
36
|
+
workout.calc_altitude_gain!
|
|
37
|
+
workout.distance = trackpoints.last.distance
|
|
38
|
+
return workout
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
data/lib/hrmparser/importer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'importer/garmin'
|
|
2
2
|
require 'importer/polar'
|
|
3
3
|
require 'importer/suunto'
|
|
4
|
+
require 'importer/gpx'
|
|
4
5
|
|
|
5
6
|
module Importer
|
|
6
7
|
def Importer.file_type(name)
|
|
@@ -11,6 +12,8 @@ module Importer
|
|
|
11
12
|
return "POLAR_HRM"
|
|
12
13
|
when /\.sdf$/i
|
|
13
14
|
return "SUUNTO"
|
|
15
|
+
when /\.gpx$/i
|
|
16
|
+
return "GPX"
|
|
14
17
|
end
|
|
15
18
|
end
|
|
16
19
|
|
data/lib/hrmparser/trackpoint.rb
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
module HRMParser
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
class TrackPoint
|
|
3
|
+
|
|
4
|
+
RAD_PER_DEG = 0.017453293 # PI/180
|
|
5
|
+
|
|
6
|
+
attr_accessor :lat, :lng, :altitude, :speed, :hr, :distance, :time, :cadence
|
|
7
|
+
def initialize(opts = {:lat => nil, :lng => nil, :altitude => nil, :speed => nil, :hr => nil, :distance => nil, :cadence => nil, :time => Time.now})
|
|
8
|
+
@lat = opts[:lat]
|
|
9
|
+
@lng = opts[:lng]
|
|
10
|
+
@altitude = opts[:altitude]
|
|
11
|
+
@speed = opts[:speed]
|
|
12
|
+
@hr = opts[:hr]
|
|
13
|
+
@distance = opts[:distance]
|
|
14
|
+
@time = opts[:time]
|
|
15
|
+
@cadence = opts[:cadence]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def calc_distance(pointA, pointB)
|
|
19
|
+
return 0 if pointA.nil? || pointA.lat.nil?
|
|
20
|
+
|
|
21
|
+
dlng = pointB.lng - pointA.lng
|
|
22
|
+
dlat = pointB.lat - pointA.lat
|
|
23
|
+
|
|
24
|
+
dlat_rad = dlat * RAD_PER_DEG
|
|
25
|
+
dlng_rad = dlng * RAD_PER_DEG
|
|
26
|
+
|
|
27
|
+
lat1_rad = pointA.lat * RAD_PER_DEG
|
|
28
|
+
lng1_rad = pointA.lng * RAD_PER_DEG
|
|
29
|
+
|
|
30
|
+
lat2_rad = pointB.lat * RAD_PER_DEG
|
|
31
|
+
lng2_rad = pointB.lng * RAD_PER_DEG
|
|
32
|
+
|
|
33
|
+
a = (Math.sin(dlat_rad/2))**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * (Math.sin(dlng_rad/2))**2
|
|
34
|
+
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
|
|
35
|
+
|
|
36
|
+
return 6371000 * c
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def calc_speed(pointA, pointB)
|
|
40
|
+
return 0 if pointA.nil? || pointA.lat.nil?
|
|
41
|
+
time_delta = pointB.time - pointA.time
|
|
42
|
+
distance_delta = pointB.distance - pointA.distance
|
|
43
|
+
return distance_delta / time_delta
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
data/spec/hrmparser_spec.rb
CHANGED
|
@@ -52,112 +52,133 @@ module HRMParser
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
context "Parse
|
|
56
|
-
it "finds
|
|
57
|
-
filename = "spec/samples/
|
|
58
|
-
data = File.read(filename)
|
|
59
|
-
importer = Importer::Garmin.new(:data => data)
|
|
60
|
-
workout = importer.restore
|
|
61
|
-
workout.time.should == Time.parse("Fri Aug 22 01:04:55 UTC 2008")
|
|
62
|
-
end
|
|
63
|
-
it "finds the duration on a short workout" do
|
|
64
|
-
filename = "spec/samples/indoor-garmin-405.TCX"
|
|
65
|
-
data = File.read(filename)
|
|
66
|
-
importer = Importer::Garmin.new(:data => data)
|
|
67
|
-
workout = importer.restore
|
|
68
|
-
workout.duration.should be_close(755, 1)
|
|
69
|
-
end
|
|
70
|
-
it "indoor workout has no trackpoints" do
|
|
71
|
-
filename = "spec/samples/indoor-garmin-405.TCX"
|
|
72
|
-
data = File.read(filename)
|
|
73
|
-
importer = Importer::Garmin.new(:data => data)
|
|
74
|
-
workout = importer.restore
|
|
75
|
-
workout.distance.should be_nil
|
|
76
|
-
workout.average_hr.should be_nil
|
|
77
|
-
workout.average_speed.should be_nil
|
|
78
|
-
workout.altitude_gain.should be_nil
|
|
79
|
-
workout.trackpoints.should == {}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Parsing the full XML is just slow. Commenting out for now.
|
|
83
|
-
it "gets workout level settings for outdoor workout" do
|
|
84
|
-
filename = "spec/samples/outdoor-garmin-405.TCX"
|
|
55
|
+
context "Parse a GPS file" do
|
|
56
|
+
it "finds the duration, time" do
|
|
57
|
+
filename = "spec/samples/gps-with-suunto.gpx"
|
|
85
58
|
data = File.read(filename)
|
|
86
|
-
importer = Importer::
|
|
59
|
+
importer = Importer::GPX.new(:data => data)
|
|
87
60
|
workout = importer.restore
|
|
88
|
-
workout.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
workout.
|
|
61
|
+
workout.time.should == Time.parse("Fri May 08 00:58:35 UTC 2009")
|
|
62
|
+
|
|
63
|
+
# Duration is actualy less, but we don't account for stopped time right now
|
|
64
|
+
workout.duration.should be_close(6382,1)
|
|
92
65
|
end
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
filename = "spec/samples/garmin-405-dies-distance.TCX"
|
|
66
|
+
it "calculates the distance and speed" do
|
|
67
|
+
filename = "spec/samples/gps-with-suunto.gpx"
|
|
96
68
|
data = File.read(filename)
|
|
97
|
-
importer = Importer::
|
|
69
|
+
importer = Importer::GPX.new(:data => data)
|
|
98
70
|
workout = importer.restore
|
|
99
|
-
workout.distance.should be_close(9426, 1)
|
|
100
|
-
workout.average_hr.should == nil
|
|
101
71
|
workout.average_speed.should be_close(6.7, 0.2)
|
|
102
|
-
workout.
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it "doesn't have any 0 in latitude" do
|
|
106
|
-
filename = "spec/samples/garmin-405-with-0-0.TCX"
|
|
107
|
-
data = File.read(filename)
|
|
108
|
-
importer = Importer::Garmin.new(:data => data)
|
|
109
|
-
workout = importer.restore
|
|
110
|
-
workout.trackpoints.map {|tp| tp.lat.should_not == 0.0}
|
|
111
|
-
workout.trackpoints.map {|tp| tp.lat.should_not == "undefined"}
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
context "Parse polar RS200 file" do
|
|
116
|
-
it "finds the duration and time" do
|
|
117
|
-
filename ="spec/samples/polarRS200.hrm"
|
|
118
|
-
data = File.read(filename)
|
|
119
|
-
importer = Importer::Polar.new(:data => data, :time_zone => "UTC")
|
|
120
|
-
workout = importer.restore
|
|
121
|
-
workout.duration.should be_close(3569,1)
|
|
122
|
-
workout.time.should == Time.parse("Thu Apr 16 12:01:55 UTC 2009")
|
|
123
|
-
end
|
|
124
|
-
it "calculates the average heartrate" do
|
|
125
|
-
filename ="spec/samples/polarRS200.hrm"
|
|
126
|
-
data = File.read(filename)
|
|
127
|
-
importer = Importer::Polar.new(:data => data, :time_zone => "UTC")
|
|
128
|
-
workout = importer.restore
|
|
129
|
-
workout.average_hr.should be_close(145, 1)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
context "Parse a Polar RR file" do
|
|
133
|
-
it "calculates the heart rate from RR" do
|
|
134
|
-
filename ="spec/samples/polarRS800-RR.hrm"
|
|
135
|
-
data = File.read(filename)
|
|
136
|
-
importer = Importer::Polar.new(:data => data, :time_zone => "UTC")
|
|
137
|
-
workout = importer.restore
|
|
138
|
-
workout.trackpoints.each {|tp| tp.hr.should < 220 && tp.hr.should > 30}
|
|
139
|
-
workout.average_hr.should be_close(115, 1)
|
|
140
|
-
workout.average_speed.should == nil
|
|
72
|
+
workout.distance.should be_close(26427, 1)
|
|
141
73
|
end
|
|
142
74
|
end
|
|
143
75
|
|
|
144
|
-
context "Parse
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
workout
|
|
76
|
+
context "Parse garmin file" do
|
|
77
|
+
it "finds workout start time on a short workout" do
|
|
78
|
+
filename = "spec/samples/indoor-garmin-405.TCX"
|
|
79
|
+
data = File.read(filename)
|
|
80
|
+
importer = Importer::Garmin.new(:data => data)
|
|
81
|
+
workout = importer.restore
|
|
82
|
+
workout.time.should == Time.parse("Fri Aug 22 01:04:55 UTC 2008")
|
|
83
|
+
end
|
|
84
|
+
it "finds the duration on a short workout" do
|
|
85
|
+
filename = "spec/samples/indoor-garmin-405.TCX"
|
|
86
|
+
data = File.read(filename)
|
|
87
|
+
importer = Importer::Garmin.new(:data => data)
|
|
88
|
+
workout = importer.restore
|
|
89
|
+
workout.duration.should be_close(755, 1)
|
|
90
|
+
end
|
|
91
|
+
it "indoor workout has no trackpoints" do
|
|
92
|
+
filename = "spec/samples/indoor-garmin-405.TCX"
|
|
93
|
+
data = File.read(filename)
|
|
94
|
+
importer = Importer::Garmin.new(:data => data)
|
|
95
|
+
workout = importer.restore
|
|
96
|
+
workout.distance.should be_nil
|
|
97
|
+
workout.average_hr.should be_nil
|
|
98
|
+
workout.average_speed.should be_nil
|
|
99
|
+
workout.altitude_gain.should be_nil
|
|
100
|
+
workout.trackpoints.should == {}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parsing the full XML is just slow. Commenting out for now.
|
|
104
|
+
it "gets workout level settings for outdoor workout" do
|
|
105
|
+
filename = "spec/samples/outdoor-garmin-405.TCX"
|
|
106
|
+
data = File.read(filename)
|
|
107
|
+
importer = Importer::Garmin.new(:data => data)
|
|
108
|
+
workout = importer.restore
|
|
109
|
+
workout.distance.should be_close(11740, 5)
|
|
110
|
+
workout.average_hr.should be_close(149.7, 0.5)
|
|
111
|
+
workout.average_speed.should be_close(1.5, 0.2)
|
|
112
|
+
workout.altitude_gain.should be_close(583, 1.0)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "gets workout level settings for weird distance workout" do
|
|
116
|
+
filename = "spec/samples/garmin-405-dies-distance.TCX"
|
|
117
|
+
data = File.read(filename)
|
|
118
|
+
importer = Importer::Garmin.new(:data => data)
|
|
119
|
+
workout = importer.restore
|
|
120
|
+
workout.distance.should be_close(9426, 1)
|
|
121
|
+
workout.average_hr.should == nil
|
|
122
|
+
workout.average_speed.should be_close(6.7, 0.2)
|
|
123
|
+
workout.altitude_gain.should be_close(40, 1.0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "doesn't have any 0 in latitude" do
|
|
127
|
+
filename = "spec/samples/garmin-405-with-0-0.TCX"
|
|
128
|
+
data = File.read(filename)
|
|
129
|
+
importer = Importer::Garmin.new(:data => data)
|
|
130
|
+
workout = importer.restore
|
|
131
|
+
workout.trackpoints.map {|tp| tp.lat.should_not == 0.0}
|
|
132
|
+
workout.trackpoints.map {|tp| tp.lat.should_not == "undefined"}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
context "Parse polar RS200 file" do
|
|
137
|
+
it "finds the duration and time" do
|
|
138
|
+
filename ="spec/samples/polarRS200.hrm"
|
|
139
|
+
data = File.read(filename)
|
|
140
|
+
importer = Importer::Polar.new(:data => data, :time_zone => "UTC")
|
|
141
|
+
workout = importer.restore
|
|
142
|
+
workout.duration.should be_close(3569,1)
|
|
143
|
+
workout.time.should == Time.parse("Thu Apr 16 12:01:55 UTC 2009")
|
|
144
|
+
end
|
|
145
|
+
it "calculates the average heartrate" do
|
|
146
|
+
filename ="spec/samples/polarRS200.hrm"
|
|
147
|
+
data = File.read(filename)
|
|
148
|
+
importer = Importer::Polar.new(:data => data, :time_zone => "UTC")
|
|
149
|
+
workout = importer.restore
|
|
150
|
+
workout.average_hr.should be_close(145, 1)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
context "Parse a Polar RR file" do
|
|
154
|
+
it "calculates the heart rate from RR" do
|
|
155
|
+
filename ="spec/samples/polarRS800-RR.hrm"
|
|
156
|
+
data = File.read(filename)
|
|
157
|
+
importer = Importer::Polar.new(:data => data, :time_zone => "UTC")
|
|
158
|
+
workout = importer.restore
|
|
159
|
+
workout.trackpoints.each {|tp| tp.hr.should < 220 && tp.hr.should > 30}
|
|
160
|
+
workout.average_hr.should be_close(115, 1)
|
|
161
|
+
workout.average_speed.should == nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
context "Parse a Suunto T6C RR file" do
|
|
166
|
+
it "finds the duration and time" do
|
|
167
|
+
filename = "spec/samples/suunto-t6-RR-stops.sdf"
|
|
168
|
+
data = File.read(filename)
|
|
169
|
+
importer = Importer::Suunto.new(:data => data, :time_zone => "-0700")
|
|
170
|
+
workout = importer.restore
|
|
171
|
+
workout.duration.should be_close(4781,1)
|
|
172
|
+
workout.time.should == Time.parse("Thu May 07 14:16:07 -0700 2009")
|
|
173
|
+
end
|
|
174
|
+
it "calculates the average HR" do
|
|
175
|
+
filename = "spec/samples/suunto-t6-RR-stops.sdf"
|
|
176
|
+
data = File.read(filename)
|
|
177
|
+
importer = Importer::Suunto.new(:data => data, :time_zone => "-0700")
|
|
178
|
+
workout = importer.restore
|
|
179
|
+
workout.average_hr.should be_close(152,1)
|
|
180
|
+
workout.average_speed.should == nil
|
|
181
|
+
end
|
|
160
182
|
end
|
|
161
|
-
end
|
|
162
183
|
end
|
|
163
184
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: teich-hrmparser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oren Teich
|
|
@@ -9,7 +9,7 @@ autorequire:
|
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
|
|
12
|
-
date: 2009-05-
|
|
12
|
+
date: 2009-05-09 00:00:00 -07:00
|
|
13
13
|
default_executable:
|
|
14
14
|
dependencies: []
|
|
15
15
|
|
|
@@ -30,6 +30,7 @@ files:
|
|
|
30
30
|
- lib/hrmparser/arraymath.rb
|
|
31
31
|
- lib/hrmparser/importer.rb
|
|
32
32
|
- lib/hrmparser/importer/garmin.rb
|
|
33
|
+
- lib/hrmparser/importer/gpx.rb
|
|
33
34
|
- lib/hrmparser/importer/polar.rb
|
|
34
35
|
- lib/hrmparser/importer/suunto.rb
|
|
35
36
|
- lib/hrmparser/trackpoint.rb
|