postrunner 0.0.11 → 0.1.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.
- 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
|
+
|