postrunner 0.0.11 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -2
- data/Rakefile +18 -2
- data/lib/postrunner/ActivitiesDB.rb +1 -22
- data/lib/postrunner/Activity.rb +21 -0
- data/lib/postrunner/ActivityLink.rb +1 -1
- data/lib/postrunner/ActivityListView.rb +10 -9
- data/lib/postrunner/ActivitySummary.rb +42 -15
- data/lib/postrunner/ActivityView.rb +10 -8
- data/lib/postrunner/ChartView.rb +238 -80
- data/lib/postrunner/DataSources.rb +1 -14
- data/lib/postrunner/DeviceList.rb +18 -10
- data/lib/postrunner/DirUtils.rb +33 -0
- data/lib/postrunner/EventList.rb +149 -0
- data/lib/postrunner/FFS_Activity.rb +297 -0
- data/lib/postrunner/FFS_Device.rb +129 -0
- data/lib/postrunner/FitFileStore.rb +372 -0
- data/lib/postrunner/HRV_Analyzer.rb +178 -0
- data/lib/postrunner/LinearPredictor.rb +46 -0
- data/lib/postrunner/Main.rb +135 -33
- data/lib/postrunner/Percentiles.rb +45 -0
- data/lib/postrunner/PersonalRecords.rb +203 -114
- data/lib/postrunner/RecordListPageView.rb +6 -6
- data/lib/postrunner/UserProfileView.rb +4 -0
- data/lib/postrunner/version.rb +1 -1
- data/misc/postrunner/trackview.js +99 -0
- data/postrunner.gemspec +5 -5
- data/spec/ActivitySummary_spec.rb +15 -4
- data/spec/FitFileStore_spec.rb +133 -0
- data/spec/FlexiTable_spec.rb +1 -1
- data/spec/PersonalRecords_spec.rb +206 -0
- data/spec/PostRunner_spec.rb +64 -60
- data/spec/View_spec.rb +1 -1
- data/spec/spec_helper.rb +76 -2
- metadata +42 -28
@@ -55,7 +55,7 @@ module PostRunner
|
|
55
55
|
start_time = session.start_time
|
56
56
|
@fit_activity.data_sources.each do |source|
|
57
57
|
t.cell(secsToHMS(source.timestamp - start_time))
|
58
|
-
t.cell(distance(source.timestamp))
|
58
|
+
t.cell(@activity.distance(source.timestamp, @unit_system))
|
59
59
|
t.cell(source.mode)
|
60
60
|
t.cell(device_name(source.distance))
|
61
61
|
t.cell(device_name(source.speed))
|
@@ -81,19 +81,6 @@ module PostRunner
|
|
81
81
|
''
|
82
82
|
end
|
83
83
|
|
84
|
-
def distance(timestamp)
|
85
|
-
@fit_activity.records.each do |record|
|
86
|
-
if record.timestamp >= timestamp
|
87
|
-
unit = { :metric => 'km', :statute => 'mi'}[@unit_system]
|
88
|
-
value = record.get_as('distance', unit)
|
89
|
-
return '-' unless value
|
90
|
-
return "#{'%.2f %s' % [value, unit]}"
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
'-'
|
95
|
-
end
|
96
|
-
|
97
84
|
end
|
98
85
|
|
99
86
|
end
|
@@ -55,19 +55,29 @@ module PostRunner
|
|
55
55
|
|
56
56
|
def devices
|
57
57
|
tables = []
|
58
|
-
|
58
|
+
unique_devices = []
|
59
|
+
# Search the device list from back to front and save the first occurance
|
60
|
+
# of each device index.
|
59
61
|
@fit_activity.device_infos.reverse_each do |device|
|
60
|
-
|
62
|
+
unless unique_devices.find { |d| d.device_index == device.device_index }
|
63
|
+
unique_devices << device
|
64
|
+
end
|
65
|
+
end
|
61
66
|
|
67
|
+
unique_devices.sort { |d1, d2| d1.device_index <=>
|
68
|
+
d2.device_index }.each do |device|
|
62
69
|
tables << (t = FlexiTable.new)
|
63
|
-
|
70
|
+
if tables.length != unique_devices.length
|
71
|
+
t.set_html_attrs(:style, 'margin-bottom: 15px')
|
72
|
+
end
|
64
73
|
t.body
|
65
74
|
|
66
75
|
t.cell('Index:', { :width => '40%' })
|
67
76
|
t.cell(device.device_index.to_s, { :width => '60%' })
|
68
77
|
t.new_row
|
69
78
|
|
70
|
-
if (manufacturer = device.manufacturer)
|
79
|
+
if (manufacturer = device.manufacturer) &&
|
80
|
+
manufacturer != 'Undocumented value 0'
|
71
81
|
t.cell('Manufacturer:', { :width => '40%' })
|
72
82
|
t.cell(manufacturer.upcase, { :width => '60%' })
|
73
83
|
t.new_row
|
@@ -75,7 +85,7 @@ module PostRunner
|
|
75
85
|
|
76
86
|
if (product = %w( garmin dynastream dynastream_oem ).include?(
|
77
87
|
device.manufacturer) ? device.garmin_product : device.product) &&
|
78
|
-
product != 0xFFFF
|
88
|
+
product != 0xFFFF && product != 0
|
79
89
|
# For unknown products the numerical ID will be returned.
|
80
90
|
product = product.to_s unless product.is_a?(String)
|
81
91
|
t.cell('Product:')
|
@@ -100,9 +110,9 @@ module PostRunner
|
|
100
110
|
t.new_row
|
101
111
|
end
|
102
112
|
|
103
|
-
if device.software_version
|
113
|
+
if (version = device.software_version) && version != 0.0
|
104
114
|
t.cell('Software Version:')
|
105
|
-
t.cell(
|
115
|
+
t.cell(version)
|
106
116
|
t.new_row
|
107
117
|
end
|
108
118
|
|
@@ -123,11 +133,9 @@ module PostRunner
|
|
123
133
|
t.cell(secsToDHMS(device.cum_operating_time))
|
124
134
|
t.new_row
|
125
135
|
end
|
126
|
-
|
127
|
-
seen_indexes << device.device_index
|
128
136
|
end
|
129
137
|
|
130
|
-
tables
|
138
|
+
tables
|
131
139
|
end
|
132
140
|
|
133
141
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = DirUtils.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
module PostRunner
|
16
|
+
|
17
|
+
module DirUtils
|
18
|
+
|
19
|
+
def create_directory(dir, name)
|
20
|
+
return if Dir.exists?(dir)
|
21
|
+
|
22
|
+
Log.info "Creating #{name} directory #{dir}"
|
23
|
+
begin
|
24
|
+
FileUtils.mkdir_p(dir)
|
25
|
+
rescue StandardError
|
26
|
+
Log.fatal "Cannot create #{name} directory #{dir}: #{$!}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = EventList.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'fit4ruby'
|
14
|
+
|
15
|
+
require 'postrunner/FlexiTable'
|
16
|
+
require 'postrunner/ViewFrame'
|
17
|
+
require 'postrunner/DeviceList'
|
18
|
+
|
19
|
+
module PostRunner
|
20
|
+
|
21
|
+
# The EventList objects can generate a table that lists all the recorded
|
22
|
+
# FIT file events in chronological order.
|
23
|
+
class EventList
|
24
|
+
|
25
|
+
include Fit4Ruby::Converters
|
26
|
+
|
27
|
+
# Create a DataSources object.
|
28
|
+
# @param activity [Activity] The activity to analyze.
|
29
|
+
# @param unit_system [Symbol] The unit system to use (:metric or
|
30
|
+
# :imperial )
|
31
|
+
def initialize(activity, unit_system)
|
32
|
+
@activity = activity
|
33
|
+
@fit_activity = activity.fit_activity
|
34
|
+
@unit_system = unit_system
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return the list as ASCII table
|
38
|
+
def to_s
|
39
|
+
list.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add the list as HTML table to the specified doc.
|
43
|
+
# @param doc [HTMLBuilder] HTML document
|
44
|
+
def to_html(doc)
|
45
|
+
ViewFrame.new("Events", 600, list).to_html(doc)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def list
|
51
|
+
session = @fit_activity.sessions[0]
|
52
|
+
|
53
|
+
t = FlexiTable.new
|
54
|
+
t.enable_frame(false)
|
55
|
+
t.body
|
56
|
+
t.row([ 'Time', 'Distance', 'Description', 'Value' ])
|
57
|
+
t.set_column_attributes([
|
58
|
+
{ :halign => :right },
|
59
|
+
{ :halign => :right },
|
60
|
+
{ :halign => :left },
|
61
|
+
{ :halign => :right }
|
62
|
+
])
|
63
|
+
start_time = session.start_time
|
64
|
+
@fit_activity.events.each do |event|
|
65
|
+
t.cell(secsToHMS(event.timestamp - start_time))
|
66
|
+
t.cell(@activity.distance(event.timestamp, @unit_system))
|
67
|
+
event_name_and_value(t, event)
|
68
|
+
t.new_row
|
69
|
+
end
|
70
|
+
|
71
|
+
t
|
72
|
+
end
|
73
|
+
|
74
|
+
def event_name_and_value(table, event)
|
75
|
+
case event.event
|
76
|
+
when 'timer'
|
77
|
+
name = "Timer (#{event.event_type.gsub(/_/, ' ')})"
|
78
|
+
value = event.timer_trigger
|
79
|
+
when 'course_point'
|
80
|
+
name = 'Course Point'
|
81
|
+
value = event.message_index
|
82
|
+
when 'battery'
|
83
|
+
name = 'Battery Level'
|
84
|
+
value = "#{event.battery_level} V"
|
85
|
+
when 'hr_high_alert'
|
86
|
+
name = 'HR high alert'
|
87
|
+
value = "#{event.hr_high_alert} bpm"
|
88
|
+
when 'hr_low_alert'
|
89
|
+
name = 'HR low alert'
|
90
|
+
value = "#{event.hr_low_alert} bpm"
|
91
|
+
when 'speed_high_alert'
|
92
|
+
name = 'Speed high alert'
|
93
|
+
value = event.speed_high_alert
|
94
|
+
when 'speed_low_alert'
|
95
|
+
name = 'Speed low alert'
|
96
|
+
value = event.speed_low_alert
|
97
|
+
when 'cad_high_alert'
|
98
|
+
name = 'Cadence high alert'
|
99
|
+
value = "#{event.cad_high_alert} spm"
|
100
|
+
when 'cad_low_alert'
|
101
|
+
name = 'Cadence low alert'
|
102
|
+
value = "#{event.cad_low_alert} spm"
|
103
|
+
when 'power_high_alert'
|
104
|
+
name = 'Power high alert'
|
105
|
+
value = event.power_high_alert
|
106
|
+
when 'power_low_alert'
|
107
|
+
name = 'Power low alert'
|
108
|
+
value = event.power_low_alert
|
109
|
+
when 'time_duration_alert'
|
110
|
+
name = 'Time duration alert'
|
111
|
+
value = event.time_duration_alert
|
112
|
+
when 'calorie_duration_alert'
|
113
|
+
name = 'Calorie duration alert'
|
114
|
+
value = event.calorie_duration_alert
|
115
|
+
when 'fitness_equipment'
|
116
|
+
name = 'Fitness equipment state'
|
117
|
+
value = event.fitness_equipment_state
|
118
|
+
when 'rider_position'
|
119
|
+
name 'Rider position changed'
|
120
|
+
value = event.rider_position
|
121
|
+
when 'comm_timeout'
|
122
|
+
name 'Communication timeout'
|
123
|
+
value = event.comm_timeout
|
124
|
+
when 'recovery_hr'
|
125
|
+
name = 'Recovery heart rate'
|
126
|
+
value = "#{event.recovery_hr} bpm"
|
127
|
+
when 'recovery_time'
|
128
|
+
name = 'Recovery time'
|
129
|
+
value = "#{secsToDHMS(event.recovery_time * 60)}"
|
130
|
+
when 'recovery_info'
|
131
|
+
name = 'Recovery info'
|
132
|
+
mins = event.recovery_info
|
133
|
+
value = "#{secsToDHMS(mins * 60)} (#{mins < 24 * 60 ? 'Good' : 'Poor'})"
|
134
|
+
when 'vo2max'
|
135
|
+
name = 'VO2Max'
|
136
|
+
value = event.vo2max
|
137
|
+
else
|
138
|
+
name = event.event
|
139
|
+
value = event.data
|
140
|
+
end
|
141
|
+
|
142
|
+
table.cell(name)
|
143
|
+
table.cell(value)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
@@ -0,0 +1,297 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FFS_Activity.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'fit4ruby'
|
14
|
+
require 'perobs'
|
15
|
+
|
16
|
+
require 'postrunner/ActivitySummary'
|
17
|
+
require 'postrunner/DataSources'
|
18
|
+
require 'postrunner/EventList'
|
19
|
+
require 'postrunner/ActivityView'
|
20
|
+
require 'postrunner/Schema'
|
21
|
+
require 'postrunner/QueryResult'
|
22
|
+
require 'postrunner/DirUtils'
|
23
|
+
|
24
|
+
module PostRunner
|
25
|
+
|
26
|
+
# The FFS_Activity objects can store a reference to the FIT file data and
|
27
|
+
# caches some frequently used values. In some cases the cached values can be
|
28
|
+
# used to overwrite the data from the FIT file.
|
29
|
+
class FFS_Activity < PEROBS::Object
|
30
|
+
|
31
|
+
include DirUtils
|
32
|
+
|
33
|
+
@@Schemata = {
|
34
|
+
'long_date' => Schema.new('long_date', 'Date',
|
35
|
+
{ :func => 'timestamp',
|
36
|
+
:column_alignment => :left,
|
37
|
+
:format => 'date_with_weekday' }),
|
38
|
+
'sub_type' => Schema.new('sub_type', 'Subtype',
|
39
|
+
{ :func => 'activity_sub_type',
|
40
|
+
:column_alignment => :left }),
|
41
|
+
'type' => Schema.new('type', 'Type',
|
42
|
+
{ :func => 'activity_type',
|
43
|
+
:column_alignment => :left })
|
44
|
+
}
|
45
|
+
|
46
|
+
ActivityTypes = {
|
47
|
+
'generic' => 'Generic',
|
48
|
+
'running' => 'Running',
|
49
|
+
'cycling' => 'Cycling',
|
50
|
+
'transition' => 'Transition',
|
51
|
+
'fitness_equipment' => 'Fitness Equipment',
|
52
|
+
'swimming' => 'Swimming',
|
53
|
+
'basketball' => 'Basketball',
|
54
|
+
'soccer' => 'Soccer',
|
55
|
+
'tennis' => 'Tennis',
|
56
|
+
'american_football' => 'American Football',
|
57
|
+
'walking' => 'Walking',
|
58
|
+
'cross_country_skiing' => 'Cross Country Skiing',
|
59
|
+
'alpine_skiing' => 'Alpine Skiing',
|
60
|
+
'snowboarding' => 'Snowboarding',
|
61
|
+
'rowing' => 'Rowing',
|
62
|
+
'mountaineering' => 'Mountaneering',
|
63
|
+
'hiking' => 'Hiking',
|
64
|
+
'multisport' => 'Multisport',
|
65
|
+
'paddling' => 'Paddling',
|
66
|
+
'all' => 'All'
|
67
|
+
}
|
68
|
+
ActivitySubTypes = {
|
69
|
+
'generic' => 'Generic',
|
70
|
+
'treadmill' => 'Treadmill',
|
71
|
+
'street' => 'Street',
|
72
|
+
'trail' => 'Trail',
|
73
|
+
'track' => 'Track',
|
74
|
+
'spin' => 'Spin',
|
75
|
+
'indoor_cycling' => 'Indoor Cycling',
|
76
|
+
'road' => 'Road',
|
77
|
+
'mountain' => 'Mountain',
|
78
|
+
'downhill' => 'Downhill',
|
79
|
+
'recumbent' => 'Recumbent',
|
80
|
+
'cyclocross' => 'Cyclocross',
|
81
|
+
'hand_cycling' => 'Hand Cycling',
|
82
|
+
'track_cycling' => 'Track Cycling',
|
83
|
+
'indoor_rowing' => 'Indoor Rowing',
|
84
|
+
'elliptical' => 'Elliptical',
|
85
|
+
'stair_climbing' => 'Stair Climbing',
|
86
|
+
'lap_swimming' => 'Lap Swimming',
|
87
|
+
'open_water' => 'Open Water',
|
88
|
+
'flexibility_training' => 'Flexibility Training',
|
89
|
+
'strength_training' => 'Strength Training',
|
90
|
+
'warm_up' => 'Warm up',
|
91
|
+
'match' => 'Match',
|
92
|
+
'exercise' => 'Excersize',
|
93
|
+
'challenge' => 'Challenge',
|
94
|
+
'indoor_skiing' => 'Indoor Skiing',
|
95
|
+
'cardio_training' => 'Cardio Training',
|
96
|
+
'all' => 'All'
|
97
|
+
}
|
98
|
+
|
99
|
+
po_attr :device, :fit_file_name, :norecord, :name, :sport, :sub_sport,
|
100
|
+
:timestamp, :total_distance, :total_timer_time, :avg_speed
|
101
|
+
attr_reader :fit_activity
|
102
|
+
|
103
|
+
# Create a new FFS_Activity object.
|
104
|
+
# @param p [PEROBS::Handle] PEROBS handle
|
105
|
+
# @param fit_file_name [String] The fully qualified file name of the FIT
|
106
|
+
# file to add
|
107
|
+
# @param fit_entity [Fit4Ruby::FitEntity] The content of the loaded FIT
|
108
|
+
# file
|
109
|
+
def initialize(p, device, fit_file_name, fit_entity)
|
110
|
+
super(p)
|
111
|
+
|
112
|
+
self.device = device
|
113
|
+
self.fit_file_name = fit_file_name ? File.basename(fit_file_name) : nil
|
114
|
+
self.name = fit_file_name ? File.basename(fit_file_name) : nil
|
115
|
+
self.norecord = false
|
116
|
+
if (@fit_activity = fit_entity)
|
117
|
+
self.timestamp = fit_entity.timestamp
|
118
|
+
self.total_timer_time = fit_entity.total_timer_time
|
119
|
+
self.sport = fit_entity.sport
|
120
|
+
self.sub_sport = fit_entity.sub_sport
|
121
|
+
self.total_distance = fit_entity.total_distance
|
122
|
+
self.avg_speed = fit_entity.avg_speed
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Store a copy of the given FIT file in the corresponding directory.
|
127
|
+
# @param fit_file_name [String] Fully qualified name of the FIT file.
|
128
|
+
def store_fit_file(fit_file_name)
|
129
|
+
# Get the right target directory for this particular FIT file.
|
130
|
+
dir = @store['file_store'].fit_file_dir(File.basename(fit_file_name),
|
131
|
+
@device.long_uid, 'activity')
|
132
|
+
# Create the necessary directories if they don't exist yet.
|
133
|
+
create_directory(dir, 'Device activity diretory')
|
134
|
+
|
135
|
+
# Copy the file into the target directory.
|
136
|
+
begin
|
137
|
+
FileUtils.cp(fit_file_name, dir)
|
138
|
+
rescue StandardError
|
139
|
+
Log.fatal "Cannot copy #{fit_file_name} into #{dir}: #{$!}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# FFS_Activity objects are sorted by their timestamp values and then by
|
144
|
+
# their device long_uids.
|
145
|
+
def <=>(a)
|
146
|
+
@timestamp == a.timestamp ? a.device.long_uid <=> self.device.long_uid :
|
147
|
+
a.timestamp <=> @timestamp
|
148
|
+
end
|
149
|
+
|
150
|
+
def check
|
151
|
+
generate_html_report
|
152
|
+
Log.info "FIT file #{@fit_file_name} is OK"
|
153
|
+
end
|
154
|
+
|
155
|
+
def dump(filter)
|
156
|
+
load_fit_file(filter)
|
157
|
+
end
|
158
|
+
|
159
|
+
def dump(filter)
|
160
|
+
load_fit_file(filter)
|
161
|
+
end
|
162
|
+
|
163
|
+
def query(key)
|
164
|
+
unless @@Schemata.include?(key)
|
165
|
+
raise ArgumentError, "Unknown key '#{key}' requested in query"
|
166
|
+
end
|
167
|
+
|
168
|
+
schema = @@Schemata[key]
|
169
|
+
|
170
|
+
if schema.func
|
171
|
+
value = send(schema.func)
|
172
|
+
else
|
173
|
+
unless instance_variable_defined?(key)
|
174
|
+
raise ArgumentError, "Don't know how to query '#{key}'"
|
175
|
+
end
|
176
|
+
value = instance_variable_get(key)
|
177
|
+
end
|
178
|
+
|
179
|
+
QueryResult.new(value, schema)
|
180
|
+
end
|
181
|
+
|
182
|
+
def events
|
183
|
+
load_fit_file
|
184
|
+
puts EventList.new(self, @store['config']['unit_system']).to_s
|
185
|
+
end
|
186
|
+
|
187
|
+
def show
|
188
|
+
html_file = html_file_name
|
189
|
+
|
190
|
+
generate_html_report #unless File.exists?(html_file)
|
191
|
+
|
192
|
+
@store['file_store'].show_in_browser(html_file)
|
193
|
+
end
|
194
|
+
|
195
|
+
def sources
|
196
|
+
load_fit_file
|
197
|
+
puts DataSources.new(self, @store['config']['unit_system']).to_s
|
198
|
+
end
|
199
|
+
|
200
|
+
def summary
|
201
|
+
load_fit_file
|
202
|
+
puts ActivitySummary.new(self, @store['config']['unit_system'],
|
203
|
+
{ :name => @name,
|
204
|
+
:type => activity_type,
|
205
|
+
:sub_type => activity_sub_type }).to_s
|
206
|
+
end
|
207
|
+
|
208
|
+
def set(attribute, value)
|
209
|
+
case attribute
|
210
|
+
when 'name'
|
211
|
+
self.name = value
|
212
|
+
when 'type'
|
213
|
+
load_fit_file
|
214
|
+
unless ActivityTypes.values.include?(value)
|
215
|
+
Log.fatal "Unknown activity type '#{value}'. Must be one of " +
|
216
|
+
ActivityTypes.values.join(', ')
|
217
|
+
end
|
218
|
+
self.sport = ActivityTypes.invert[value]
|
219
|
+
when 'subtype'
|
220
|
+
unless ActivitySubTypes.values.include?(value)
|
221
|
+
Log.fatal "Unknown activity subtype '#{value}'. Must be one of " +
|
222
|
+
ActivitySubTypes.values.join(', ')
|
223
|
+
end
|
224
|
+
self.sub_sport = ActivitySubTypes.invert[value]
|
225
|
+
when 'norecord'
|
226
|
+
unless %w( true false).include?(value)
|
227
|
+
Log.fatal "norecord must either be 'true' or 'false'"
|
228
|
+
end
|
229
|
+
self.norecord = value == 'true'
|
230
|
+
else
|
231
|
+
Log.fatal "Unknown activity attribute '#{attribute}'. Must be one of " +
|
232
|
+
'name, type or subtype'
|
233
|
+
end
|
234
|
+
generate_html_report
|
235
|
+
end
|
236
|
+
|
237
|
+
# Return true if this activity generated any personal records.
|
238
|
+
def has_records?
|
239
|
+
!@store['records'].activity_records(self).empty?
|
240
|
+
end
|
241
|
+
|
242
|
+
def html_file_name(full_path = true)
|
243
|
+
fn = "#{@device.short_uid}_#{@fit_file_name[0..-5]}.html"
|
244
|
+
full_path ? File.join(@store['config']['html_dir'], fn) : fn
|
245
|
+
end
|
246
|
+
|
247
|
+
def generate_html_report
|
248
|
+
load_fit_file
|
249
|
+
ActivityView.new(self, @store['config']['unit_system'])
|
250
|
+
end
|
251
|
+
|
252
|
+
def activity_type
|
253
|
+
ActivityTypes[@sport] || 'Undefined'
|
254
|
+
end
|
255
|
+
|
256
|
+
def activity_sub_type
|
257
|
+
ActivitySubTypes[@sub_sport] || 'Undefined'
|
258
|
+
end
|
259
|
+
|
260
|
+
def distance(timestamp, unit_system)
|
261
|
+
load_fit_file
|
262
|
+
|
263
|
+
@fit_activity.records.each do |record|
|
264
|
+
if record.timestamp >= timestamp
|
265
|
+
unit = { :metric => 'km', :statute => 'mi'}[unit_system]
|
266
|
+
value = record.get_as('distance', unit)
|
267
|
+
return '-' unless value
|
268
|
+
return "#{'%.2f %s' % [value, unit]}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
'-'
|
273
|
+
end
|
274
|
+
|
275
|
+
def load_fit_file(filter = nil)
|
276
|
+
return if @fit_activity
|
277
|
+
|
278
|
+
dir = @store['file_store'].fit_file_dir(@fit_file_name,
|
279
|
+
@device.long_uid, 'activity')
|
280
|
+
fit_file = File.join(dir, @fit_file_name)
|
281
|
+
begin
|
282
|
+
@fit_activity = Fit4Ruby.read(fit_file, filter)
|
283
|
+
rescue Fit4Ruby::Error
|
284
|
+
Log.fatal "#{@fit_file_name} corrupted: #{$!}"
|
285
|
+
end
|
286
|
+
|
287
|
+
unless @fit_activity
|
288
|
+
Log.fatal "#{fit_file} does not contain any activity records"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
private
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|