summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/postrunner/Activity.rb12
-rw-r--r--lib/postrunner/ActivityReport.rb22
-rw-r--r--lib/postrunner/ActivityView.rb133
-rw-r--r--lib/postrunner/ChartView.rb96
-rw-r--r--lib/postrunner/FlexiTable.rb19
-rw-r--r--lib/postrunner/HTMLBuilder.rb67
-rw-r--r--lib/postrunner/TrackView.rb99
-rw-r--r--lib/postrunner/ViewWidgets.rb46
-rw-r--r--postrunner.gemspec3
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"