diff options
-rw-r--r-- | lib/postrunner/ActivitiesDB.rb | 12 | ||||
-rw-r--r-- | lib/postrunner/Activity.rb | 10 | ||||
-rw-r--r-- | lib/postrunner/ActivityListView.rb | 27 | ||||
-rw-r--r-- | lib/postrunner/ActivitySummary.rb (renamed from lib/postrunner/ActivityReport.rb) | 73 | ||||
-rw-r--r-- | lib/postrunner/ActivityView.rb | 10 | ||||
-rw-r--r-- | lib/postrunner/ChartView.rb | 57 | ||||
-rw-r--r-- | lib/postrunner/Main.rb | 16 | ||||
-rw-r--r-- | lib/postrunner/RuntimeConfig.rb | 7 | ||||
-rw-r--r-- | spec/ActivitySummary_spec.rb | 28 | ||||
-rw-r--r-- | spec/FlexiTable_spec.rb | 8 | ||||
-rw-r--r-- | spec/PostRunner_spec.rb | 65 | ||||
-rw-r--r-- | spec/spec_helper.rb | 62 |
12 files changed, 261 insertions, 114 deletions
diff --git a/lib/postrunner/ActivitiesDB.rb b/lib/postrunner/ActivitiesDB.rb index 0e21e65..81f039a 100644 --- a/lib/postrunner/ActivitiesDB.rb +++ b/lib/postrunner/ActivitiesDB.rb @@ -248,6 +248,18 @@ module PostRunner end end + # This method can be called to re-generate all HTML reports and all HTML + # index files. + def generate_all_html_reports + Log.info "Re-generating all HTML report files..." + # Generate HTML views for all activities in the DB. + @activities.each { |a| a.generate_html_view } + Log.info "All HTML report files have been re-generated." + # (Re-)generate index files. + ActivityListView.new(self).update_html_index + Log.info "HTML index files have been updated." + end + private def sync diff --git a/lib/postrunner/Activity.rb b/lib/postrunner/Activity.rb index 70ab1ca..de4396b 100644 --- a/lib/postrunner/Activity.rb +++ b/lib/postrunner/Activity.rb @@ -12,7 +12,7 @@ require 'fit4ruby' -require 'postrunner/ActivityReport' +require 'postrunner/ActivitySummary' require 'postrunner/ActivityView' module PostRunner @@ -48,8 +48,7 @@ module PostRunner end def check - # Re-generate the HTML file for this activity - generate_html_view + @fit_activity = load_fit_file Log.info "FIT file #{@fit_file} is OK" end @@ -88,7 +87,7 @@ module PostRunner def summary @fit_activity = load_fit_file unless @fit_activity - puts ActivityReport.new(self).to_s + puts ActivitySummary.new(@fit_activity, name, @db.cfg[:unit_system]).to_s end def rename(name) @@ -110,7 +109,8 @@ module PostRunner def generate_html_view @fit_activity = load_fit_file unless @fit_activity - ActivityView.new(self, @db.predecessor(self), @db.successor(self)) + ActivityView.new(self, @db.cfg[:unit_system], @db.predecessor(self), + @db.successor(self)) end private diff --git a/lib/postrunner/ActivityListView.rb b/lib/postrunner/ActivityListView.rb index 5a37c26..61275da 100644 --- a/lib/postrunner/ActivityListView.rb +++ b/lib/postrunner/ActivityListView.rb @@ -42,6 +42,7 @@ module PostRunner def initialize(db) @db = db + @unit_system = @db.cfg[:unit_system] @page_size = 20 @page_no = -1 @last_page = (@db.activities.length - 1) / @page_size @@ -130,7 +131,7 @@ EOT end def generate_table - i = @page_no * @page_size + i = @page_no < 0 ? 0 : @page_no * @page_size t = FlexiTable.new t.head t.row(%w( Ref. Activity Start Distance Duration Pace ), @@ -151,9 +152,10 @@ EOT i += 1, ActivityLink.new(a), a.timestamp.strftime("%a, %Y %b %d %H:%M"), - "%.2f" % (a.total_distance / 1000), + local_value(a.total_distance, 'm', '%.2f', + { :metric => 'km', :statute => 'mi' }), secsToHMS(a.total_timer_time), - speedToPace(a.avg_speed) ]) + pace(a.avg_speed) ]) end t @@ -168,6 +170,25 @@ EOT Log.fatal "Cannot write activity index file '#{output_file}: #{$!}" end end + + def local_value(value, from_unit, format, units) + to_unit = units[@unit_system] + return '-' unless value + value *= conversion_factor(from_unit, to_unit) + "#{format % [value, to_unit]}" + end + + def pace(speed) + case @unit_system + when :metric + "#{speedToPace(speed)}" + when :statute + "#{speedToPace(speed, 1609.34)}" + else + Log.fatal "Unknown unit system #{@unit_system}" + end + end + end end diff --git a/lib/postrunner/ActivityReport.rb b/lib/postrunner/ActivitySummary.rb index f15ed91..c1f5771 100644 --- a/lib/postrunner/ActivityReport.rb +++ b/lib/postrunner/ActivitySummary.rb @@ -1,7 +1,7 @@ #!/usr/bin/env ruby -w # encoding: UTF-8 # -# = ActivityReport.rb -- PostRunner - Manage the data from your Garmin sport devices. +# = ActivitySummary.rb -- PostRunner - Manage the data from your Garmin sport devices. # # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org> # @@ -17,25 +17,27 @@ require 'postrunner/ViewWidgets' module PostRunner - class ActivityReport + class ActivitySummary include Fit4Ruby::Converters include ViewWidgets - def initialize(activity) - @activity = activity + def initialize(fit_activity, name, unit_system) + @fit_activity = fit_activity + @name = name + @unit_system = unit_system end def to_s - session = @activity.fit_activity.sessions[0] + session = @fit_activity.sessions[0] summary(session).to_s + "\n" + laps.to_s end def to_html(doc) - session = @activity.fit_activity.sessions[0] + session = @fit_activity.sessions[0] - frame(doc, "Activity: #{@activity.name}") { + frame(doc, "Activity: #{@name}") { summary(session).to_html(doc) } frame(doc, 'Laps') { @@ -50,12 +52,17 @@ module PostRunner t.enable_frame(false) t.body t.row([ 'Date:', session.timestamp]) - t.row([ 'Distance:', "#{'%.2f' % (session.total_distance / 1000.0)} km" ]) + t.row([ 'Distance:', + local_value(session, 'total_distance', '%.2f %s', + { :metric => 'km', :statute => 'mi'}) ]) t.row([ 'Time:', secsToHMS(session.total_timer_time) ]) - t.row([ 'Avg. Pace:', - "#{speedToPace(session.avg_speed)} min/km" ]) - t.row([ 'Total Ascend:', "#{session.total_ascend} m" ]) - t.row([ 'Total Descend:', "#{session.total_descent} m" ]) + t.row([ 'Avg. Pace:', pace(session, 'avg_speed') ]) + t.row([ 'Total Ascent:', + local_value(session, 'total_ascent', '%.0f %s', + { :metric => 'm', :statute => 'ft' }) ]) + t.row([ 'Total Descent:', + local_value(session, 'total_descent', '%.0f %s', + { :metric => 'm', :statute => 'ft' }) ]) t.row([ 'Calories:', "#{session.total_calories} kCal" ]) t.row([ 'Avg. HR:', session.avg_heart_rate ? "#{session.avg_heart_rate} bpm" : '-' ]) @@ -67,17 +74,17 @@ module PostRunner session.avg_running_cadence ? "#{session.avg_running_cadence.round} spm" : '-' ]) t.row([ 'Avg. Vertical Oscillation:', - session.avg_vertical_oscillation ? - "#{'%.1f' % (session.avg_vertical_oscillation / 10)} cm" : '-' ]) + local_value(session, 'avg_vertical_oscillation', '%.1f %s', + { :metric => 'cm', :statute => 'in' }) ]) t.row([ 'Avg. Ground Contact Time:', session.avg_stance_time ? "#{session.avg_stance_time.round} ms" : '-' ]) t.row([ 'Avg. Stride Length:', - session.avg_stride_length ? - "#{'%.2f' % (session.avg_stride_length / 2)} m" : '-' ]) - rec_time = @activity.fit_activity.recovery_time + local_value(session, 'avg_stride_length', '%.2f %s', + { :metric => 'm', :statute => 'ft' }) ]) + rec_time = @fit_activity.recovery_time t.row([ 'Recovery Time:', rec_time ? secsToHMS(rec_time * 60) : '-' ]) - vo2max = @activity.fit_activity.vo2max + vo2max = @fit_activity.vo2max t.row([ 'VO2max:', vo2max ? vo2max : '-' ]) t @@ -90,13 +97,14 @@ module PostRunner 'Avg. HR', 'Max. HR' ]) t.set_column_attributes(Array.new(8, { :halign => :right })) t.body - @activity.fit_activity.sessions[0].laps.each.with_index do |lap, index| + @fit_activity.sessions[0].laps.each.with_index do |lap, index| t.cell(index + 1) t.cell(secsToHMS(lap.total_timer_time)) - t.cell('%.2f' % (lap.total_distance / 1000.0)) - t.cell(speedToPace(lap.avg_speed)) - t.cell(lap.total_strides ? - '%.2f' % (lap.total_distance / (2 * lap.total_strides)) : '') + t.cell(local_value(lap, 'total_distance', '%.2f', + { :metric => 'km', :statute => 'mi' })) + t.cell(pace(lap, 'avg_speed', false)) + t.cell(local_value(lap, 'avg_stride_length', '%.2f', + { :metric => 'm', :statute => 'ft' })) t.cell(lap.avg_running_cadence && lap.avg_fractional_cadence ? '%.1f' % (2 * lap.avg_running_cadence + (2 * lap.avg_fractional_cadence) / 100.0) : '') @@ -108,6 +116,25 @@ module PostRunner t end + def local_value(fdr, field, format, units) + unit = units[@unit_system] + value = fdr.get_as(field, unit) + return '-' unless value + "#{format % [value, unit]}" + end + + def pace(fdr, field, show_unit = true) + speed = fdr.get(field) + case @unit_system + when :metric + "#{speedToPace(speed)}#{show_unit ? ' min/km' : ''}" + when :statute + "#{speedToPace(speed, 1609.34)}#{show_unit ? ' min/mi' : ''}" + else + Log.fatal "Unknown unit system #{@unit_system}" + end + end + end end diff --git a/lib/postrunner/ActivityView.rb b/lib/postrunner/ActivityView.rb index 7cfe3fb..bde8d42 100644 --- a/lib/postrunner/ActivityView.rb +++ b/lib/postrunner/ActivityView.rb @@ -13,7 +13,7 @@ require 'fit4ruby' require 'postrunner/HTMLBuilder' -require 'postrunner/ActivityReport' +require 'postrunner/ActivitySummary' require 'postrunner/ViewWidgets' require 'postrunner/TrackView' require 'postrunner/ChartView' @@ -24,8 +24,9 @@ module PostRunner include ViewWidgets - def initialize(activity, predecessor, successor) + def initialize(activity, unit_system, predecessor, successor) @activity = activity + @unit_system = unit_system @predecessor = predecessor @successor = successor @output_dir = activity.html_dir @@ -39,9 +40,10 @@ module PostRunner private def generate_html(doc) - @report = ActivityReport.new(@activity) + @report = ActivitySummary.new(@activity.fit_activity, @activity.name, + @unit_system) @track_view = TrackView.new(@activity) - @chart_view = ChartView.new(@activity) + @chart_view = ChartView.new(@activity, @unit_system) doc.html { head(doc) diff --git a/lib/postrunner/ChartView.rb b/lib/postrunner/ChartView.rb index 3265aa1..5a68f0b 100644 --- a/lib/postrunner/ChartView.rb +++ b/lib/postrunner/ChartView.rb @@ -18,8 +18,9 @@ module PostRunner include ViewWidgets - def initialize(activity) + def initialize(activity, unit_system) @activity = activity + @unit_system = unit_system @empty_charts = {} end @@ -34,16 +35,29 @@ module PostRunner end def div(doc) - chart_div(doc, 'pace', 'Pace (min/km)') - chart_div(doc, 'altitude', 'Elevation (m)') + chart_div(doc, 'pace', "Pace (#{select_unit('min/km')})") + chart_div(doc, 'altitude', "Elevation (#{select_unit('m')})") chart_div(doc, 'heart_rate', 'Heart Rate (bpm)') - chart_div(doc, 'cadence', 'Run Cadence (spm)') - chart_div(doc, 'vertical_oscillation', 'Vertical Oscillation (cm)') + chart_div(doc, 'run_cadence', 'Run Cadence (spm)') + chart_div(doc, 'vertical_oscillation', + "Vertical Oscillation (#{select_unit('cm')})") chart_div(doc, 'stance_time', 'Ground Contact Time (ms)') end private + def select_unit(metric_unit) + case @unit_system + when :metric + metric_unit + when :statute + { 'min/km' => 'min/mi', 'm' => 'ft', 'cm' => 'in', + 'bpm' => 'bpm', 'spm' => 'spm', 'ms' => 'ms' }[metric_unit] + else + Log.fatal "Unknown unit system #{@unit_system}" + end + end + def style <<EOT .chart-container { @@ -77,22 +91,22 @@ EOT def java_script s = "$(function() {\n" - s << line_graph('pace', '#0A7BEE' ) - s << line_graph('altitude', '#5AAA44') - s << line_graph('heart_rate', '#900000') - s << point_graph('cadence', + s << line_graph('pace', 'min/km', '#0A7BEE' ) + s << line_graph('altitude', 'm', '#5AAA44') + s << line_graph('heart_rate', 'bpm', '#900000') + s << point_graph('run_cadence', 'spm', [ [ '#EE3F2D', 151 ], [ '#F79666', 163 ], [ '#A0D488', 174 ], [ '#96D7DE', 185 ], - [ '#A88BBB', nil ] ], 2) - s << point_graph('vertical_oscillation', - [ [ '#A88BBB', 6.7 ], - [ '#96D7DE', 8.4 ], - [ '#A0D488', 10.1 ], - [ '#F79666', 11.8 ], - [ '#EE3F2D', nil ] ], 0.1) - s << point_graph('stance_time', + [ '#A88BBB', nil ] ]) + s << point_graph('vertical_oscillation', 'cm', + [ [ '#A88BBB', 67 ], + [ '#96D7DE', 84 ], + [ '#A0D488', 101 ], + [ '#F79666', 118 ], + [ '#EE3F2D', nil ] ]) + s << point_graph('stance_time', 'ms', [ [ '#A88BBB', 208 ], [ '#96D7DE', 241 ], [ '#A0D488', 273 ], @@ -104,13 +118,13 @@ EOT s end - def line_graph(field, color = nil) + def line_graph(field, unit, color = nil) s = "var #{field}_data = [\n" data_set = [] start_time = @activity.fit_activity.sessions[0].start_time.to_i @activity.fit_activity.records.each do |r| - value = r.send(field) + value = r.get_as(field, select_unit(unit)) if field == 'pace' if value > 20.0 value = nil @@ -148,7 +162,7 @@ EOT s << "});\n" end - def point_graph(field, colors, multiplier = 1) + def point_graph(field, unit, colors) # We need to split the field values into separate data sets for each # color. The max value for each color determines which set a data point # ends up in. @@ -162,7 +176,6 @@ EOT @activity.fit_activity.records.each do |r| # Undefined values will be discarded. next unless (value = r.send(field)) - value *= multiplier # Find the right set by looking at the maximum allowed values for each # color. @@ -173,7 +186,7 @@ EOT # allowed range for this set, so add the value as x/y pair to the # set. x_val = (r.timestamp.to_i - start_time) * 1000 - data_sets[i] << [ x_val, value ] + data_sets[i] << [ x_val, r.get_as(field, select_unit(unit)) ] # Abort the color loop since we've found the right set already. break end diff --git a/lib/postrunner/Main.rb b/lib/postrunner/Main.rb index a2e6d11..25d9cd0 100644 --- a/lib/postrunner/Main.rb +++ b/lib/postrunner/Main.rb @@ -144,6 +144,9 @@ show [ <ref> ] summary <ref> Display the summary information for the FIT file. +units metric | statute + Change the unit system. + <fit file> An absolute or relative name of a .FIT file. @@ -200,6 +203,8 @@ EOT end when 'summary' process_activities(args, :summary) + when 'units' + change_unit_system(args) when nil Log.fatal("No command provided. " + "See 'postrunner -h' for more information.") @@ -292,6 +297,17 @@ EOT end end + def change_unit_system(args) + if args.length != 1 || !%w( metric statute ).include?(args[0]) + Log.fatal("You must specify 'metric' or 'statute' as unit system.") + end + + if @cfg[:unit_system].to_s != args[0] + @cfg.set_option(:unit_system, args[0].to_sym) + @activities.generate_all_html_reports + end + end + end end diff --git a/lib/postrunner/RuntimeConfig.rb b/lib/postrunner/RuntimeConfig.rb index 0cc3e05..2bb9cb0 100644 --- a/lib/postrunner/RuntimeConfig.rb +++ b/lib/postrunner/RuntimeConfig.rb @@ -31,6 +31,13 @@ module PostRunner load_options if File.exist?(@config_file) end + # Shortcut for get_option. + # @param name [Symbol] the name of the config option. + # @return [Object] the value of the config option. + def [](name) + get_option(name) + end + # Get a config option value. # @param name [Symbol] the name of the config option. # @return [Object] the value of the config option. diff --git a/spec/ActivitySummary_spec.rb b/spec/ActivitySummary_spec.rb new file mode 100644 index 0000000..4e92017 --- /dev/null +++ b/spec/ActivitySummary_spec.rb @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = PostRunner_spec.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# + +require 'postrunner/ActivitySummary' +require 'spec_helper' + +describe PostRunner::ActivitySummary do + + before(:each) do + @as = PostRunner::ActivitySummary.new( + create_fit_activity('2014-08-26-19:00', 30), 'test', :metric) + end + + it 'should create a metric summary' do + @as.to_s #TODO: Fix aggregation first + end + +end + diff --git a/spec/FlexiTable_spec.rb b/spec/FlexiTable_spec.rb index f08e59a..5a03c56 100644 --- a/spec/FlexiTable_spec.rb +++ b/spec/FlexiTable_spec.rb @@ -19,7 +19,13 @@ describe PostRunner::FlexiTable do row(%w( a bb )) row(%w( ccc ddddd )) end - puts t.to_s + ref = <<EOT ++---+-----+ +|a |bb | +|ccc|ddddd| ++---+-----+ +EOT + t.to_s.should == ref end end diff --git a/spec/PostRunner_spec.rb b/spec/PostRunner_spec.rb index 1fbfd05..9755a7c 100644 --- a/spec/PostRunner_spec.rb +++ b/spec/PostRunner_spec.rb @@ -13,6 +13,7 @@ require 'fileutils' require 'postrunner/Main' +require 'spec_helper' describe PostRunner::Main do @@ -25,62 +26,6 @@ describe PostRunner::Main do stdout.string end - def create_fit_file(name, date) - ts = Time.parse(date) - a = Fit4Ruby::Activity.new({ :timestamp => ts }) - a.total_timer_time = 30 * 60 - a.new_user_profile({ :timestamp => ts, - :age => 33, :height => 1.78, :weight => 73.0, - :gender => 'male', :activity_class => 4.0, - :max_hr => 178 }) - - a.new_event({ :timestamp => ts, :event => 'timer', - :event_type => 'start_time' }) - a.new_device_info({ :timestamp => ts, :device_index => 0 }) - a.new_device_info({ :timestamp => ts, :device_index => 1, - :battery_status => 'ok' }) - 0.upto(a.total_timer_time / 60) do |mins| - ts += 60 - a.new_record({ - :timestamp => ts, - :position_lat => 51.5512 - mins * 0.0008, - :position_long => 11.647 + mins * 0.002, - :distance => 200.0 * mins, - :altitude => 100 + mins * 0.5, - :speed => 3.1, - :vertical_oscillation => 9 + mins * 0.02, - :stance_time => 235.0 * mins * 0.01, - :stance_time_percent => 32.0, - :heart_rate => 140 + mins, - :cadence => 75, - :activity_type => 'running', - :fractional_cadence => (mins % 2) / 2.0 - }) - - if mins > 0 && mins % 5 == 0 - a.new_lap({ :timestamp => ts }) - end - end - a.new_session({ :timestamp => ts }) - a.new_event({ :timestamp => ts, :event => 'recovery_time', - :event_type => 'marker', - :data => 2160 }) - a.new_event({ :timestamp => ts, :event => 'vo2max', - :event_type => 'marker', :data => 52 }) - a.new_event({ :timestamp => ts, :event => 'timer', - :event_type => 'stop_all' }) - a.new_device_info({ :timestamp => ts, :device_index => 0 }) - ts += 1 - a.new_device_info({ :timestamp => ts, :device_index => 1, - :battery_status => 'low' }) - ts += 120 - a.new_event({ :timestamp => ts, :event => 'recovery_hr', - :event_type => 'marker', :data => 132 }) - - a.aggregate - Fit4Ruby.write(name, a) - end - before(:all) do @db_dir = File.join(File.dirname(__FILE__), '.postrunner') FileUtils.rm_rf(@db_dir) @@ -169,5 +114,13 @@ describe PostRunner::Main do postrunner(%w( dump FILE1.FIT )) end + it 'should switch to statute units' do + postrunner(%w( units statute )) + end + + it 'should switch back to metric units' do + postrunner(%w( units metric )) + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..cd6fec4 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,62 @@ +def create_fit_file(name, date, duration_minutes = 30) + Fit4Ruby.write(name, create_fit_activity(date, duration_minutes)) +end + +def create_fit_activity(date, duration_minutes) + ts = Time.parse(date) + a = Fit4Ruby::Activity.new({ :timestamp => ts }) + a.total_timer_time = duration_minutes * 60 + a.new_user_profile({ :timestamp => ts, + :age => 33, :height => 1.78, :weight => 73.0, + :gender => 'male', :activity_class => 4.0, + :max_hr => 178 }) + + a.new_event({ :timestamp => ts, :event => 'timer', + :event_type => 'start_time' }) + a.new_device_info({ :timestamp => ts, :device_index => 0 }) + a.new_device_info({ :timestamp => ts, :device_index => 1, + :battery_status => 'ok' }) + 0.upto((a.total_timer_time / 60) - 1) do |mins| + a.new_record({ + :timestamp => ts, + :position_lat => 51.5512 - mins * 0.0008, + :position_long => 11.647 + mins * 0.002, + :distance => 200.0 * mins, + :altitude => 100 + mins * 0.5, + :speed => 3.1, + :vertical_oscillation => 9 + mins * 0.02, + :stance_time => 235.0 * mins * 0.01, + :stance_time_percent => 32.0, + :heart_rate => 140 + mins, + :cadence => 75, + :activity_type => 'running', + :fractional_cadence => (mins % 2) / 2.0 + }) + + ts += 60 + if (mins + 1) % 5 == 0 + a.new_lap({ :timestamp => ts }) + end + end + a.new_session({ :timestamp => ts }) + a.new_event({ :timestamp => ts, :event => 'recovery_time', + :event_type => 'marker', + :data => 2160 }) + a.new_event({ :timestamp => ts, :event => 'vo2max', + :event_type => 'marker', :data => 52 }) + a.new_event({ :timestamp => ts, :event => 'timer', + :event_type => 'stop_all' }) + a.new_device_info({ :timestamp => ts, :device_index => 0 }) + ts += 1 + a.new_device_info({ :timestamp => ts, :device_index => 1, + :battery_status => 'low' }) + ts += 120 + a.new_event({ :timestamp => ts, :event => 'recovery_hr', + :event_type => 'marker', :data => 132 }) + + a.aggregate + + a +end + + |