diff options
-rw-r--r-- | lib/postrunner/Activity.rb | 12 | ||||
-rw-r--r-- | lib/postrunner/ActivityReport.rb | 22 | ||||
-rw-r--r-- | lib/postrunner/ActivityView.rb | 133 | ||||
-rw-r--r-- | lib/postrunner/ChartView.rb | 96 | ||||
-rw-r--r-- | lib/postrunner/FlexiTable.rb | 19 | ||||
-rw-r--r-- | lib/postrunner/HTMLBuilder.rb | 67 | ||||
-rw-r--r-- | lib/postrunner/TrackView.rb | 99 | ||||
-rw-r--r-- | lib/postrunner/ViewWidgets.rb | 46 | ||||
-rw-r--r-- | postrunner.gemspec | 3 |
9 files changed, 369 insertions, 128 deletions
diff --git a/lib/postrunner/Activity.rb b/lib/postrunner/Activity.rb index c60a557..f616298 100644 --- a/lib/postrunner/Activity.rb +++ b/lib/postrunner/Activity.rb @@ -1,8 +1,7 @@ require 'fit4ruby' require 'postrunner/ActivityReport' -require 'postrunner/TrackView' -require 'postrunner/ChartView' +require 'postrunner/ActivityView' module PostRunner @@ -63,10 +62,11 @@ module PostRunner def show @fit_activity = load_fit_file unless @fit_activity - view = TrackView.new(self, '../../html') - view.generate_html - chart = ChartView.new(self, '../../html') - chart.generate_html + view = ActivityView.new(self, File.join(@db.db_dir, 'html')) + #view = TrackView.new(self, '../../html') + #view.generate_html + #chart = ChartView.new(self, '../../html') + #chart.generate_html end def summary diff --git a/lib/postrunner/ActivityReport.rb b/lib/postrunner/ActivityReport.rb index 1369aec..d66f631 100644 --- a/lib/postrunner/ActivityReport.rb +++ b/lib/postrunner/ActivityReport.rb @@ -1,11 +1,14 @@ require 'fit4ruby' + require 'postrunner/FlexiTable' +require 'postrunner/ViewWidgets' module PostRunner class ActivityReport include Fit4Ruby::Converters + include ViewWidgets def initialize(activity) @activity = activity @@ -14,7 +17,18 @@ module PostRunner def to_s session = @activity.sessions[0] - summary(session) + "\n" + laps + summary(session).to_s + "\n" + laps.to_s + end + + def to_html(doc) + session = @activity.sessions[0] + + frame(doc, 'Summary') { + summary(session).to_html(doc) + } + frame(doc, 'Laps') { + laps.to_html(doc) + } end private @@ -54,8 +68,7 @@ module PostRunner vo2max = @activity.vo2max t.row([ 'VO2max:', vo2max ? vo2max : '-' ]) - - t.to_s + t end def laps @@ -79,7 +92,8 @@ module PostRunner t.cell(lap.max_heart_rate.to_s) t.new_row end - t.to_s + + t end end diff --git a/lib/postrunner/ActivityView.rb b/lib/postrunner/ActivityView.rb new file mode 100644 index 0000000..6def7dc --- /dev/null +++ b/lib/postrunner/ActivityView.rb @@ -0,0 +1,133 @@ +require 'fit4ruby' + +require 'postrunner/HTMLBuilder' +require 'postrunner/ActivityReport' +require 'postrunner/ViewWidgets' +require 'postrunner/TrackView' +require 'postrunner/ChartView' + +module PostRunner + + class ActivityView + + include ViewWidgets + + def initialize(activity, output_dir) + @activity = activity + @output_dir = output_dir + @output_file = nil + + ensure_output_dir + + @doc = HTMLBuilder.new + generate_html(@doc) + write_file + show_in_browser + end + + private + + def ensure_output_dir + unless Dir.exists?(@output_dir) + begin + Dir.mkdir(@output_dir) + rescue SystemCallError + Log.fatal "Cannot create output directory '#{@output_dir}': #{$!}" + end + end + end + + def generate_html(doc) + @report = ActivityReport.new(@activity.fit_activity) + @track_view = TrackView.new(@activity) + @chart_view = ChartView.new(@activity) + + doc.html { + head(doc) + body(doc) + } + end + + def head(doc) + doc.head { + doc.meta({ 'http-equiv' => 'Content-Type', + 'content' => 'text/html; charset=utf-8' }) + doc.meta({ 'name' => 'viewport', + 'content' => 'width=device-width, ' + + 'initial-scale=1.0, maximum-scale=1.0, ' + + 'user-scalable=0' }) + doc.title("PostRunner Activity: #{@activity.name}") + style(doc) + view_widgets_style(doc) + @chart_view.head(doc) + @track_view.head(doc) + } + end + + def style(doc) + doc.style(<<EOT +.main { + width: 1210px; + margin: 0 auto; +} +.left_col { + float: left; + width: 400px; +} +.right_col { + float: right; + width: 600px; +} +.widget_container { + box-sizing: border-box; + width: 600px; + padding: 10px 15px 15px 15px; + margin: 15px auto 15px auto; + border: 1px solid #ddd; + background: #fff; + background: linear-gradient(#f6f6f6 0, #fff 50px); + background: -o-linear-gradient(#f6f6f6 0, #fff 50px); + background: -ms-linear-gradient(#f6f6f6 0, #fff 50px); + background: -moz-linear-gradient(#f6f6f6 0, #fff 50px); + background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px); + box-shadow: 0 3px 10px rgba(0,0,0,0.15); + -o-box-shadow: 0 3px 10px rgba(0,0,0,0.1); + -ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1); + -moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1); + -webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1); +} +EOT + ) + end + + def body(doc) + doc.body({ 'onload' => 'init()' }) { + doc.div({ 'class' => 'main' }) { + doc.div({ 'class' => 'left_col' }) { + @report.to_html(doc) + @track_view.div(doc) + } + doc.div({ 'class' => 'right_col' }) { + @chart_view.div(doc) + } + } + } + end + + def write_file + @output_file = File.join(@output_dir, "#{@activity.fit_file[0..-5]}.html") + begin + File.write(@output_file, @doc.to_html) + rescue IOError + Log.fatal "Cannot write activity view file '#{@output_file}: #{$!}" + end + end + + def show_in_browser + system("firefox \"#{@output_file}\" &") + end + + end + +end + diff --git a/lib/postrunner/ChartView.rb b/lib/postrunner/ChartView.rb index f25a731..1ccdc2d 100644 --- a/lib/postrunner/ChartView.rb +++ b/lib/postrunner/ChartView.rb @@ -1,20 +1,38 @@ +require 'postrunner/ViewWidgets' + module PostRunner class ChartView - def initialize(activity, output_dir) + include ViewWidgets + + def initialize(activity) @activity = activity - @output_dir = output_dir end - def generate_html - s = <<EOT -<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> -<html> -<head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> - <title>Flot Examples: Basic Usage</title> - <style> + def head(doc) + [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js', + 'flot/jquery.flot.time.js' ].each do |js| + doc.script({ 'language' => 'javascript', 'type' => 'text/javascript', + 'src' => js }) + end + doc.style(style) + doc.script(java_script) + end + + def div(doc) + chart_div(doc, 'pace', 'Pace (min/km)') + chart_div(doc, 'altitude', 'Elevation (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, 'stance_time', 'Ground Contact Time (ms)') + end + + private + + def style + <<EOT .chart-container { box-sizing: border-box; width: 600px; @@ -35,22 +53,16 @@ module PostRunner -webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1); } .chart-placeholder { - width: 100%; - height: 100%; + width: 570px; + height: 200px; font-size: 14px; line-height: 1.2em; } - </style> - <script language="javascript" type="text/javascript" -src="js/jquery-2.1.1.js"></script> - <script language="javascript" type="text/javascript" -src="js/flot/jquery.flot.js"></script> - <script language="javascript" type="text/javascript" - src="js/flot/jquery.flot.time.js"></script> - <script type="text/javascript"> - - $(function() { EOT + end + + def java_script + s = "$(function() {\n" s << line_graph('pace', '#0A7BEE' ) s << line_graph('altitude', '#5AAA44') @@ -74,35 +86,11 @@ EOT [ '#F79666', 305 ], [ '#EE3F2D', nil ] ]) - s << <<EOT - }); - </script> -</head> -<body> - <div id="header"> - <h2>HR Chart</h2> - </div> -EOT - - s << chart_div('pace', 'Pace (min/km)') - s << chart_div('altitude', 'Elevation (m)') - s << chart_div('heart_rate', 'Heart Rate (bpm)') - s << chart_div('cadence', 'Run Cadence (spm)') - s << chart_div('vertical_oscillation', 'Vertical Oscillation (cm)') - s << chart_div('stance_time', 'Ground Contact Time (ms)') - - s << "</body>\n</html>\n" + s << "\n});\n" - file_name = File.join(@output_dir, "#{@activity_id}_hr.html") - begin - File.write(file_name, s) - rescue IOError - Log.fatal "Cannot write chart view '#{file_name}': #{$!}" - end + s end - private - def line_graph(field, color = nil) s = "var #{field}_data = [\n" @@ -195,14 +183,10 @@ EOT s end - def chart_div(field, title) - " <div id=\"#{field}_content\">\n" + - " <div class=\"chart-container\">\n" + - " <b>#{title}</b>\n" + - " <div id=\"#{field}_chart\" class=\"chart-placeholder\">" + - "</div>\n" + - " </div>\n" + - " </div>\n" + def chart_div(doc, field, title) + frame(doc, title) { + doc.div({ 'id' => "#{field}_chart", 'class' => 'chart-placeholder'}) + } end end diff --git a/lib/postrunner/FlexiTable.rb b/lib/postrunner/FlexiTable.rb index 2fa45c4..d921496 100644 --- a/lib/postrunner/FlexiTable.rb +++ b/lib/postrunner/FlexiTable.rb @@ -1,3 +1,5 @@ +require 'postrunner/HTMLBuilder' + module PostRunner class FlexiTable @@ -67,7 +69,9 @@ module PostRunner end end - def to_html + def to_html(doc) + doc.td(@content.respond_to?('to_html') ? + @content.to_html(doc) : @content.to_s) end private @@ -115,6 +119,12 @@ module PostRunner s end + def to_html(doc) + doc.tr { + each { |c| c.to_html(doc) } + } + end + end attr_reader :frame, :column_attributes @@ -206,7 +216,12 @@ module PostRunner s end - def to_html + def to_html(doc) + doc.table { + @head_rows.each { |r| r.to_html(doc) } + @body_rows.each { |r| r.to_html(doc) } + @foot_rows.each { |r| r.to_html(doc) } + } end private diff --git a/lib/postrunner/HTMLBuilder.rb b/lib/postrunner/HTMLBuilder.rb new file mode 100644 index 0000000..e02f4b3 --- /dev/null +++ b/lib/postrunner/HTMLBuilder.rb @@ -0,0 +1,67 @@ +require 'nokogiri' + +module PostRunner + + # Nokogiri is great, but I don't like the HTMLBuilder interface. This class + # is a wrapper around Nokogiri that provides a more Ruby-like interface. + class HTMLBuilder + + # Create a new HTMLBuilder object. + def initialize + # This is the Nokogiri Document that will store all the data. + @doc = Nokogiri::HTML::Document.new + # We only need to keep a stack of the currently edited nodes so we know + # where we are in the node tree. + @node_stack = [] + end + + # Any call to an undefined method will create a HTML node of the same + # name. + def method_missing(method_name, *args) + node = Nokogiri::XML::Node.new(method_name.to_s, @doc) + if (parent = @node_stack.last) + parent.add_child(node) + else + @doc.add_child(node) + end + @node_stack.push(node) + + args.each do |arg| + if arg.is_a?(String) + node.add_child(Nokogiri::XML::Text.new(arg, @doc)) + elsif arg.is_a?(Hash) + # Hash arguments are attribute sets for the node. We just pass them + # directly to the node. + arg.each { |k, v| node[k] = v } + end + end + + yield if block_given? + @node_stack.pop + end + + # Only needed to comply with style guides. This all calls to unknown + # method will be handled properly. So, we always return true. + def respond_to?(method) + true + end + + # Dump the HTML document as HTML formatted String. + def to_html + @doc.to_html + end + + private + + def add_child(parent, node) + if parent + parent.add_child(node) + else + @doc.add_child(node) + end + end + + end + +end + diff --git a/lib/postrunner/TrackView.rb b/lib/postrunner/TrackView.rb index 1110a29..50e314d 100644 --- a/lib/postrunner/TrackView.rb +++ b/lib/postrunner/TrackView.rb @@ -1,71 +1,50 @@ require 'fit4ruby' +require 'postrunner/ViewWidgets' + module PostRunner class TrackView - def initialize(activity, output_dir) + include ViewWidgets + + def initialize(activity) @activity = activity - @activity_id = activity.fit_file[0..-4] - @output_dir = output_dir end - def generate_html - s = <<EOT -<!DOCTYPE html> -<html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> - <meta name="viewport" content="width=device-width, - initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> - <meta name="apple-mobile-web-app-capable" content="yes"> -EOT - s << "<title>PostRunner: #{@activity.name}</title>\n" - s << <<EOT - <link rel="stylesheet" href="js/theme/default/style.css" type="text/css"> - <link rel="stylesheet" href="js/theme/default/google.css" type="text/css"> - <style> + def head(doc) + doc.link({ 'rel' => 'stylesheet', + 'href' => 'openlayers/theme/default/style.css', + 'type' => 'text/css' }) + doc.style(style) + doc.script({ 'src' => 'openlayers/OpenLayers.js' }) + doc.script(java_script) + end + + def div(doc) + frame(doc, 'Map') { + doc.div({ 'id' => 'map', 'class' => 'trackmap' }) + } + end + + private + + def style + <<EOT .olControlAttribution { bottom: 5px; } .trackmap { - width: 600px; - height: 400px; - border: 1px solid #ccc; + width: 570px; + height: 400px; + border: 2px solid #545454; } - </style> - <script src="js/OpenLayers.js"></script> - <script> EOT - s << js_file - - s << <<EOT - </script> - </head> - <body onload="init()"> -EOT - s << "<h1 id=\"title\">PostRunner: #{@activity.name}</h1>\n" - s << <<EOT - <p id="shortdesc"> - Map view of a captured track. - </p> - <div id="map" class="trackmap"></div> - </body> -</html> -EOT - file_name = File.join(@output_dir, "#{@activity_id}.html") - begin - File.write(file_name, s) - rescue IOError - Log.fatal "Cannot write TrackViewer file '#{file_name}': #{$!}" - end end - private - - def js_file - script = <<EOT + def java_script + js = <<EOT var map; function init() { @@ -80,7 +59,7 @@ EOT (session.nec_lat - session.swc_lat) / 2.0 last_lap = @activity.fit_activity.laps[-1] - script << <<EOT + js << <<EOT map = new OpenLayers.Map({ div: "map", projection: mercator, @@ -89,15 +68,15 @@ EOT zoom: 13 }); EOT - script << <<"EOT" + js << <<"EOT" track_layer = new OpenLayers.Layer.PointTrack("Track", {style: {strokeColor: '#FF0000', strokeWidth: 5}}); map.addLayer(track_layer); track_layer.addNodes([ EOT - track_points(script) + track_points(js) - script << <<"EOT" + js << <<"EOT" ]); var markers = new OpenLayers.Layer.Markers( "Markers" ); map.addLayer(markers); @@ -105,17 +84,17 @@ EOT var size = new OpenLayers.Size(21,25); var offset = new OpenLayers.Pixel(-(size.w/2), -size.h); EOT - set_marker(script, 'marker-green', session.start_position_long, + set_marker(js, 'marker-green', session.start_position_long, session.start_position_lat) @activity.fit_activity.laps[0..-2].each do |lap| - set_marker(script, 'marker-blue', + set_marker(js, 'marker-blue', lap.end_position_long, lap.end_position_lat) end - set_marker(script, 'marker', + set_marker(js, 'marker', last_lap.end_position_long, last_lap.end_position_lat) - script << "\n};" + js << "\n};" - script + js end def track_points(script) @@ -140,7 +119,7 @@ EOT def set_marker(script, type, long, lat) script << <<"EOT" - markers.addMarker(new OpenLayers.Marker(new OpenLayers.LonLat(#{long},#{lat}).transform(geographic, mercator),new OpenLayers.Icon('js/img/#{type}.png',size,offset))); + markers.addMarker(new OpenLayers.Marker(new OpenLayers.LonLat(#{long},#{lat}).transform(geographic, mercator),new OpenLayers.Icon('openlayers/img/#{type}.png',size,offset))); EOT end diff --git a/lib/postrunner/ViewWidgets.rb b/lib/postrunner/ViewWidgets.rb new file mode 100644 index 0000000..27a7886 --- /dev/null +++ b/lib/postrunner/ViewWidgets.rb @@ -0,0 +1,46 @@ +module PostRunner + + module ViewWidgets + + def view_widgets_style(doc) + doc.style(<<EOT +.widget_frame { + box-sizing: border-box; + width: 600px; + padding: 10px 15px 15px 15px; + margin: 15px auto 15px auto; + border: 1px solid #ddd; + background: #fff; + background: linear-gradient(#f6f6f6 0, #fff 50px); + background: -o-linear-gradient(#f6f6f6 0, #fff 50px); + background: -ms-linear-gradient(#f6f6f6 0, #fff 50px); + background: -moz-linear-gradient(#f6f6f6 0, #fff 50px); + background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px); + box-shadow: 0 3px 10px rgba(0,0,0,0.15); + -o-box-shadow: 0 3px 10px rgba(0,0,0,0.1); + -ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1); + -moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1); + -webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1); +} +.widget_frame_title { + padding-bottom: 5px; +} +EOT + ) + end + + def frame(doc, title) + doc.div({ 'class' => 'widget_frame' }) { + doc.div({ 'class' => 'widget_frame_title' }) { + doc.b(title) + } + doc.div { + yield if block_given? + } + } + end + + end + +end + diff --git a/postrunner.gemspec b/postrunner.gemspec index ab4f3e2..4f1aca6 100644 --- a/postrunner.gemspec +++ b/postrunner.gemspec @@ -19,6 +19,9 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.required_ruby_version = '>=2.0' + spec.add_dependency "fit4ruby" + spec.add_dependency "nokogiri", "~> 1.6" + spec.add_development_dependency "bundler", "~> 1.6" spec.add_development_dependency "rake" spec.add_development_dependency "rspec" |