From a3bc76ca40be53a47e530ed91819e9b8483de7bb Mon Sep 17 00:00:00 2001 From: Chris Schlaeger Date: Sun, 12 Apr 2015 20:22:01 +0200 Subject: New: Adding a personal record view including yearly records. --- lib/postrunner/ActivitiesDB.rb | 21 +- lib/postrunner/Activity.rb | 3 +- lib/postrunner/ActivityLink.rb | 59 ++ lib/postrunner/ActivityListView.rb | 126 +-- lib/postrunner/ActivitySummary.rb | 12 +- lib/postrunner/ActivityView.rb | 124 ++- lib/postrunner/ChartView.rb | 30 +- lib/postrunner/DeviceList.rb | 10 +- lib/postrunner/FlexiTable.rb | 29 +- lib/postrunner/HTMLBuilder.rb | 80 +- lib/postrunner/NavButtonRow.rb | 103 +++ lib/postrunner/PagingButtons.rb | 77 ++ lib/postrunner/PersonalRecords.rb | 47 +- lib/postrunner/RecordListPageView.rb | 69 ++ lib/postrunner/TrackView.rb | 30 +- lib/postrunner/UserProfileView.rb | 8 +- lib/postrunner/View.rb | 97 +++ lib/postrunner/ViewBottom.rb | 54 ++ lib/postrunner/ViewButtons.rb | 68 ++ lib/postrunner/ViewFrame.rb | 93 ++ lib/postrunner/ViewTop.rb | 80 ++ lib/postrunner/ViewWidgets.rb | 153 ---- misc/icons/activities.png | Bin 0 -> 427 bytes misc/icons/activities.svg | 1582 ++++++++++++++++++++++++++++++++++ spec/View_spec.rb | 61 ++ 25 files changed, 2616 insertions(+), 400 deletions(-) create mode 100644 lib/postrunner/ActivityLink.rb create mode 100644 lib/postrunner/NavButtonRow.rb create mode 100644 lib/postrunner/PagingButtons.rb create mode 100644 lib/postrunner/RecordListPageView.rb create mode 100644 lib/postrunner/View.rb create mode 100644 lib/postrunner/ViewBottom.rb create mode 100644 lib/postrunner/ViewButtons.rb create mode 100644 lib/postrunner/ViewFrame.rb create mode 100644 lib/postrunner/ViewTop.rb delete mode 100644 lib/postrunner/ViewWidgets.rb create mode 100644 misc/icons/activities.png create mode 100644 misc/icons/activities.svg create mode 100644 spec/View_spec.rb diff --git a/lib/postrunner/ActivitiesDB.rb b/lib/postrunner/ActivitiesDB.rb index f63e30a..b6516e7 100644 --- a/lib/postrunner/ActivitiesDB.rb +++ b/lib/postrunner/ActivitiesDB.rb @@ -18,12 +18,13 @@ require 'postrunner/BackedUpFile' require 'postrunner/Activity' require 'postrunner/PersonalRecords' require 'postrunner/ActivityListView' +require 'postrunner/ViewButtons' module PostRunner class ActivitiesDB - attr_reader :db_dir, :cfg, :fit_dir, :activities, :records + attr_reader :db_dir, :cfg, :fit_dir, :activities, :records, :views def initialize(db_dir, cfg) @db_dir = db_dir @@ -59,6 +60,14 @@ module PostRunner sync_needed |= !a.fit_activity.nil? end + # Define which View objects the HTML output will contain off. This + # doesn't really belong in ActivitiesDB but for now it's the best place + # to put it. + @views = ViewButtons.new([ + NavButtonDef.new('activities.png', 'index.html'), + NavButtonDef.new('record.png', "records-0.html") + ]) + @records = PersonalRecords.new(self) sync if sync_needed end @@ -164,12 +173,12 @@ module PostRunner def check @records.delete_all_records - @activities.sort! do |a1, a2| + @activities.sort do |a1, a2| a1.timestamp <=> a2.timestamp end.each { |a| a.check } @records.sync # Ensure that HTML index is up-to-date. - ActivityListView.new(self).update_html_index + ActivityListView.new(self).update_index_pages end def ref_by_fit_file(fit_file) @@ -265,7 +274,7 @@ module PostRunner # Show the activity list in a web browser. def show_list_in_browser - ActivityListView.new(self).update_html_index + ActivityListView.new(self).update_index_pages show_in_browser(File.join(@cfg[:html_dir], 'index.html')) end @@ -300,7 +309,7 @@ module PostRunner @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 + ActivityListView.new(self).update_index_pages Log.info "HTML index files have been updated." end @@ -332,7 +341,7 @@ module PostRunner end @records.sync - ActivityListView.new(self).update_html_index + ActivityListView.new(self).update_index_pages end def create_directory(dir, name) diff --git a/lib/postrunner/Activity.rb b/lib/postrunner/Activity.rb index e0e1e06..b4a38f4 100644 --- a/lib/postrunner/Activity.rb +++ b/lib/postrunner/Activity.rb @@ -312,8 +312,7 @@ module PostRunner def generate_html_view @fit_activity = load_fit_file unless @fit_activity - ActivityView.new(self, @db.cfg[:unit_system], @db.predecessor(self), - @db.successor(self)) + ActivityView.new(self, @db.cfg[:unit_system]) end def activity_type diff --git a/lib/postrunner/ActivityLink.rb b/lib/postrunner/ActivityLink.rb new file mode 100644 index 0000000..9c368b0 --- /dev/null +++ b/lib/postrunner/ActivityLink.rb @@ -0,0 +1,59 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = ActivityLink.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2014, 2015 by Chris Schlaeger +# +# 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/HTMLBuilder' + +module PostRunner + + # Generates the name of an Activity with a link to the ActivityReport. + # Optionally, an icon can be shown for Activities that contain a current + # personal record. + class ActivityLink + + def initialize(activity, show_record_icon = false) + @activity = activity + @show_record_icon = show_record_icon + end + + # Add the ActivityLink as HTML Elements to the document. + # @param doc [HTMLBuilder] XML Document + def to_html(doc) + doc.unique(:activitylink_style) { doc.style(style) } + + doc.a(@activity.name, { :class => 'activity_link', + :href => @activity.fit_file[0..-5] + '.html' }) + if @show_record_icon && @activity.has_records? + doc.img(nil, { :src => 'icons/record-small.png', + :style => 'vertical-align:middle' }) + end + end + + # Convert the ActivityLink into a plain text form. Return the first 20 + # characters of the Activity name. + def to_s + @activity.name[0..19] + end + + private + + def style + < 'activity_link', - :href => @activity.fit_file[0..-5] + '.html' }) - if @activity.has_records? - doc.img(nil, { :src => 'icons/record-small.png', - :style => 'vertical-align:middle' }) - end - end - - def to_s - @activity.name[0..19] - end - - end - include Fit4Ruby::Converters - include ViewWidgets def initialize(db) @db = db @@ -52,85 +32,42 @@ module PostRunner @last_page = (@db.activities.length - 1) / @page_size end - def update_html_index + def update_index_pages 0.upto(@last_page) do |page_no| @page_no = page_no - generate_html_index_page + generate_html_index_page(page_no) end end - def to_html(doc) - generate_table.to_html(doc) - end - def to_s generate_table.to_s end private - def generate_html_index_page - doc = HTMLBuilder.new + def generate_html_index_page(page_index) + views = @db.views + views.current_page = 'index.html' - doc.html { - head(doc) - body(doc) - } - - write_file(doc) - end + pages = PagingButtons.new((0..@last_page).map do |i| + "index#{i == 0 ? '' : "-#{i}"}.html" + end) + pages.current_page = + "index#{page_index == 0 ? '' : "-#{page_index}"}.html" + @view = View.new("PostRunner Activities", views, pages) - def head(doc) - doc.head { - doc.meta({ 'http-equiv' => 'Content-Type', - 'content' => 'text/html; charset=utf-8' }) - doc.title("PostRunner Activities") - style(doc) - } - end + @view.doc.head { @view.doc.style(style) } + body(@view.doc) - def style(doc) - view_widgets_style(doc) - doc.style(< 'main' }) { - frame(doc, 'Activities') { - generate_table.to_html(doc) - } + ViewFrame.new('Activities', 900, generate_table).to_html(doc) } - footer(doc) } end @@ -154,7 +91,7 @@ EOT activities.each do |a| t.row([ i += 1, - ActivityLink.new(a), + ActivityLink.new(a, true), a.activity_type, a.timestamp.strftime("%a, %Y %b %d %H:%M"), local_value(a.total_distance, 'm', '%.2f', @@ -168,14 +105,19 @@ EOT t end - def write_file(doc) - output_file = File.join(@db.cfg[:html_dir], - "index#{@page_no == 0 ? '' : @page_no}.html") - begin - File.write(output_file, doc.to_html) - rescue IOError - Log.fatal "Cannot write activity index file '#{output_file}: #{$!}" - end + def style + < +# Copyright (c) 2014, 2015 by Chris Schlaeger # # 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 @@ -12,70 +12,75 @@ require 'fit4ruby' -require 'postrunner/HTMLBuilder' +require 'postrunner/View' require 'postrunner/ActivitySummary' require 'postrunner/DeviceList' require 'postrunner/UserProfileView' -require 'postrunner/ViewWidgets' require 'postrunner/TrackView' require 'postrunner/ChartView' module PostRunner - class ActivityView + class ActivityView < View - include ViewWidgets - - def initialize(activity, unit_system, predecessor, successor) + def initialize(activity, unit_system) @activity = activity + db = @activity.db @unit_system = unit_system - @predecessor = predecessor - @successor = successor - @output_dir = activity.db.cfg[:html_dir] - @output_file = nil - @doc = HTMLBuilder.new + views = db.views + views.current_page = nil + + # Sort activities in reverse order so the newest one is considered the + # last report by the pagin buttons. + activities = db.activities.sort do |a1, a2| + a1.timestamp <=> a2.timestamp + end + + pages = PagingButtons.new(activities.map do |a| + "#{a.fit_file[0..-5]}.html" + end, false) + pages.current_page = "#{@activity.fit_file[0..-5]}.html" + + super("PostRunner Activity: #{@activity.name}", views, pages) generate_html(@doc) - write_file + write(File.join(db.cfg[:html_dir], pages.current_page)) end private def generate_html(doc) - @report = ActivitySummary.new(@activity.fit_activity, @unit_system, - { :name => @activity.name, - :type => @activity.activity_type, - :sub_type => @activity.activity_sub_type - }) - @device_list = DeviceList.new(@activity.fit_activity) - @user_profile = UserProfileView.new(@activity.fit_activity, @unit_system) - @track_view = TrackView.new(@activity) - @chart_view = ChartView.new(@activity, @unit_system) - - 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}") - view_widgets_style(doc) - @chart_view.head(doc) - @track_view.head(doc) - style(doc) + doc.head { doc.style(style) } + #doc.meta({ 'name' => 'viewport', + # 'content' => 'width=device-width, ' + + # 'initial-scale=1.0, maximum-scale=1.0, ' + + # 'user-scalable=0' }) + + body { + doc.body({ :onload => 'init()' }) { + # The main area with the 2 column layout. + doc.div({ :class => 'main' }) { + doc.div({ :class => 'left_col' }) { + ActivitySummary.new(@activity.fit_activity, @unit_system, + { :name => @activity.name, + :type => @activity.activity_type, + :sub_type => @activity.activity_sub_type + }).to_html(doc) + TrackView.new(@activity).to_html(doc) + DeviceList.new(@activity.fit_activity).to_html(doc) + UserProfileView.new(@activity.fit_activity, @unit_system). + to_html(doc) + } + doc.div({ :class => 'right_col' }) { + ChartView.new(@activity, @unit_system).to_html(doc) + } + } + } } end - def style(doc) - doc.style(< 'init()' }) { - prev_page = @predecessor ? @predecessor.fit_file[0..-5] + '.html' : nil - next_page = @successor ? @successor.fit_file[0..-5] + '.html' : nil - titlebar(doc, nil, prev_page, 'index.html', next_page) - # The main area with the 2 column layout. - doc.div({ :class => 'main' }) { - doc.div({ :class => 'left_col' }) { - @report.to_html(doc) - @track_view.div(doc) - @device_list.to_html(doc) - @user_profile.to_html(doc) - } - doc.div({ :class => 'right_col' }) { - @chart_view.div(doc) - } - } - footer(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 end diff --git a/lib/postrunner/ChartView.rb b/lib/postrunner/ChartView.rb index 1aa6039..33e413d 100644 --- a/lib/postrunner/ChartView.rb +++ b/lib/postrunner/ChartView.rb @@ -9,14 +9,10 @@ # published by the Free Software Foundation. # -require 'postrunner/ViewWidgets' - module PostRunner class ChartView - include ViewWidgets - def initialize(activity, unit_system) @activity = activity @sport = activity.fit_activity.sessions[0].sport @@ -24,17 +20,19 @@ module PostRunner @empty_charts = {} end - 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 to_html(doc) + doc.unique(:chartview_style) { + doc.head { + [ '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) + } + } - def div(doc) + doc.script(java_script) if @sport == 'running' chart_div(doc, 'pace', "Pace (#{select_unit('min/km')})") else @@ -274,9 +272,9 @@ EOT # Don't plot frame for graph without data. return if @empty_charts[field] - frame(doc, title) { + ViewFrame.new(title) { doc.div({ 'id' => "#{field}_chart", 'class' => 'chart-placeholder'}) - } + }.to_html(doc) end def hover_function(chart_id, y_label, y_unit) diff --git a/lib/postrunner/DeviceList.rb b/lib/postrunner/DeviceList.rb index 3316dde..6ab63f2 100644 --- a/lib/postrunner/DeviceList.rb +++ b/lib/postrunner/DeviceList.rb @@ -3,7 +3,7 @@ # # = DeviceList.rb -- PostRunner - Manage the data from your Garmin sport devices. # -# Copyright (c) 2014 by Chris Schlaeger +# Copyright (c) 2014, 2015 by Chris Schlaeger # # 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 @@ -12,22 +12,18 @@ require 'fit4ruby' -require 'postrunner/ViewWidgets' +require 'postrunner/ViewFrame' module PostRunner class DeviceList - include ViewWidgets - def initialize(fit_activity) @fit_activity = fit_activity end def to_html(doc) - frame(doc, 'Devices') { - devices.each { |d| d.to_html(doc) } - } + ViewFrame.new('Devices', 600, devices).to_html(doc) end def to_s diff --git a/lib/postrunner/FlexiTable.rb b/lib/postrunner/FlexiTable.rb index 7385db7..8113cc1 100644 --- a/lib/postrunner/FlexiTable.rb +++ b/lib/postrunner/FlexiTable.rb @@ -3,7 +3,7 @@ # # = FlexiTable.rb -- PostRunner - Manage the data from your Garmin sport devices. # -# Copyright (c) 2014 by Chris Schlaeger +# Copyright (c) 2014, 2015 by Chris Schlaeger # # 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 @@ -252,6 +252,9 @@ module PostRunner def to_html(doc) index_table + doc.unique(:flexitable_style) { + doc.head { doc.style(style) } + } doc.table(@html_attrs) { @head_rows.each { |r| r.to_html(doc) } @body_rows.each { |r| r.to_html(doc) } @@ -318,6 +321,30 @@ module PostRunner s + "\n" end + def style + < 'Content-Type', + 'content' => 'text/html; charset=utf-8' }) + create_node('title', title) + } + @body = create_node('body') + } + @node_stack << @html + @node_stack << @body + end + + # Append nodes provided in block to head section of HTML document. + def head + @node_stack.push(@head) + yield if block_given? + unless @node_stack.pop == @head + raise ArgumentError, "node_stack corrupted in head" + end + end + + # Append nodes provided in block to body section of HTML document. + def body(*args) + @node_stack.push(@body) + args.each do |arg| + if arg.is_a?(Hash) + arg.each { |k, v| @body[k] = v } + end + end + yield if block_given? + unless @node_stack.pop == @body + raise ArgumentError, "node_stack corrupted in body" + end + end + + # Only execute the passed block if the provided tag has not been added + # yet. + def unique(tag) + unless @tags.include?(tag) + @tags << tag + yield if block_given? + end 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) + def method_missing(method_name, *args, &block) + create_node(method_name.to_s, *args, &block) + 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 create_node(name, *args) + node = Nokogiri::XML::Node.new(name, @doc) if (parent = @node_stack.last) parent.add_child(node) else @@ -52,19 +113,6 @@ module PostRunner @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) diff --git a/lib/postrunner/NavButtonRow.rb b/lib/postrunner/NavButtonRow.rb new file mode 100644 index 0000000..7e1ea06 --- /dev/null +++ b/lib/postrunner/NavButtonRow.rb @@ -0,0 +1,103 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = NavButtonRow.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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/HTMLBuilder' + +module PostRunner + + # Auxilliary class that stores the name of an icon file and a URL as a + # String. It is used to describe a NavButtonRow button. + class NavButtonDef < Struct.new(:icon, :url) + end + + # A NavButtonRow is a row of buttons used to navigate between HTML pages. + class NavButtonRow + + # A class to store the icon and URL of a button in the NavButtonRow + # objects. + class Button + + # Create a Button object. + # @param icon [String] File name of the icon file + # @param url [String] URL of the page to change to + def initialize(icon, url = nil) + @icon = icon + @url = url + end + + # Add the object as HTML Elements to the document. + # @param doc [HTMLBuilder] XML Document + def to_html(doc) + if @url + doc.a({ :href => @url }) { + doc.img({ :src => "icons/#{@icon}", :class => 'active_button' }) + } + else + doc.img({ :src => "icons/#{@icon}", :class => 'inactive_button' }) + end + end + + end + + # Create a new NavButtonRow object. + # @param float [String, Nil] specifies if the HTML representation should + # be a floating object that floats left or right. + def initialize(float = nil) + unless float.nil? || %w( left right ).include?(float) + raise ArgumentError "float argument must be nil, 'left' or 'right'" + end + + @float = float + @buttons = [] + end + + # Add a new button to the NavButtonRow object. + # @param icon [String] File name of the icon file + # @param url [String] URL of the page to change to + def addButton(icon, url = nil) + @buttons << Button.new(icon, url) + end + + # Add the object as HTML Elements to the document. + # @param doc [HTMLBuilder] XML Document + def to_html(doc) + doc.unique(:nav_button_row_style) { + doc.head { doc.style(style) } + } + doc.div({ :class => 'nav_button_row', + :style => "width: #{@buttons.length * (32 + 10)}px; " + + "#{@float ? "float: #{@float};" : + 'margin-left: auto; margin-right: auto'}"}) { + @buttons.each { |btn| btn.to_html(doc) } + } + end + + private + + def style + <<"EOT" +.nav_button_row { + padding: 3px 30px; +} +.active_button { + padding: 5px; +} +.inactive_button { + padding: 5px; + opacity: 0.4; +} +EOT + end + + end + +end diff --git a/lib/postrunner/PagingButtons.rb b/lib/postrunner/PagingButtons.rb new file mode 100644 index 0000000..aa1f5f2 --- /dev/null +++ b/lib/postrunner/PagingButtons.rb @@ -0,0 +1,77 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = PagingButtons.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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/NavButtonRow' + +module PostRunner + + # A class to generate a set of forward/backward buttons for an HTML page. It + # can also include jump to first/last buttons. + class PagingButtons + + # Create a new PagingButtons object. + # @param page_urls [Array of String] Sorted list of all possible pages + # @param end_buttons [Boolean] If true jump to first/last buttons are + # included + def initialize(page_urls, end_buttons = true) + if page_urls.empty? + raise ArgumentError.new("'page_urls' must not be empty") + end + @pages = page_urls + @current_page_index = 0 + @end_buttons = end_buttons + end + + # Return the URL of the current page + def current_page + @pages[@current_page_index] + end + + # Set the URL for the current page. It must be included in the URL set + # passed at creation time. The forward/backward links will be derived from + # the setting of the current page. + # @param page_url [String] URL of the page + def current_page=(page_url) + unless (@current_page_index = @pages.index(page_url)) + raise ArgumentError.new("URL #{page_url} is not a known page URL") + end + end + + # Iterate over all buttons. A NavButtonDef object is passed to the block + # that contains the icon and URL for the button. If no URL is set, the + # button is inactive. + def each + %w( first back forward last ).each do |button_name| + button = NavButtonDef.new + button.icon = button_name + '.png' + button.url = + case button_name + when 'first' + @current_page_index == 0 || !@end_buttons ? nil : @pages.first + when 'back' + @current_page_index == 0 ? nil : + @pages[@current_page_index - 1] + when 'forward' + @current_page_index == @pages.length - 1 ? nil : + @pages[@current_page_index + 1] + when 'last' + @current_page_index == @pages.length - 1 || + !@end_buttons ? nil : @pages.last + end + + yield(button) + end + end + + end + +end diff --git a/lib/postrunner/PersonalRecords.rb b/lib/postrunner/PersonalRecords.rb index aba2f6a..7f69529 100644 --- a/lib/postrunner/PersonalRecords.rb +++ b/lib/postrunner/PersonalRecords.rb @@ -15,6 +15,8 @@ require 'yaml' require 'fit4ruby' require 'postrunner/BackedUpFile' +require 'postrunner/RecordListPageView' +require 'postrunner/ActivityLink' module PostRunner @@ -89,7 +91,8 @@ module PostRunner secsToHMS(@duration), speedToPace(@distance / @duration) ]) + [ @activity.db.ref_by_fit_file(@activity.fit_file), - @activity.name, @start_time.strftime("%Y-%m-%d") ]) + ActivityLink.new(@activity, false), + @start_time.strftime("%Y-%m-%d") ]) end end @@ -171,6 +174,16 @@ module PostRunner def to_s return '' if empty? + generate_table.to_s + "\n" + end + + def to_html(doc) + generate_table.to_html(doc) + end + + private + + def generate_table t = FlexiTable.new t.head t.row([ 'Record', 'Time/Dist.', 'Avg. Pace', 'Ref.', 'Activity', @@ -192,13 +205,17 @@ module PostRunner records.sort { |r1, r2| r1.distance <=> r2.distance }.each do |r| r.to_table_row(t) end - t.to_s + "\n" + + t end + end class SportRecords + attr_reader :sport, :all_time, :yearly + def initialize(sport) @sport = sport @all_time = RecordSet.new(@sport, nil) @@ -251,6 +268,22 @@ module PostRunner str end + def to_html(doc) + return nil if empty? + + doc.div { + doc.h3('All-time records') + @all_time.to_html(doc) + @yearly.values.sort{ |r1, r2| r2.year <=> r1.year }.each do |record| + puts record.year + unless record.empty? + doc.h3("Records of #{record.year}") + record.to_html(doc) + end + end + } + end + end def initialize(activities) @@ -286,6 +319,16 @@ module PostRunner def sync save_records + + non_empty_records = @sport_records.select { |s, r| !r.empty? } + max = non_empty_records.length + i = 0 + non_empty_records.each do |sport, record| + output_file = File.join(@activities.cfg[:html_dir], + "records-#{i}.html") + RecordListPageView.new(@activities, record, max, i). + write(output_file) + end end def to_s diff --git a/lib/postrunner/RecordListPageView.rb b/lib/postrunner/RecordListPageView.rb new file mode 100644 index 0000000..f1de004 --- /dev/null +++ b/lib/postrunner/RecordListPageView.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = RecordListPageView.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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 'fit4ruby' + +require 'postrunner/FlexiTable' +require 'postrunner/View' +require 'postrunner/ViewFrame' +require 'postrunner/ViewButtons' +require 'postrunner/PagingButtons' + +module PostRunner + + # Generates an HTML page with all personal records for a particular sport + # type. + class RecordListPageView < View + + include Fit4Ruby::Converters + + # Create a RecordListPageView object. + # @param db [ActivityDB] Activity database + # @param records [PersonalRecords] Database with personal records + # @param page_count [Fixnum] Number of total pages + # @param page_index [Fixnum] Index of the page + def initialize(db, records, page_count, page_index) + @db = db + @unit_system = @db.cfg[:unit_system] + @records = records + + views = @db.views + views.current_page = "records-0.html" + + pages = PagingButtons.new((0..(page_count - 1)).map do |i| + "records-#{i}.html" + end) + pages.current_page = + "records-#{page_index}.html" + + @sport_name = Activity::ActivityTypes[@records.sport] + super("#{@sport_name} Records", views, pages) + + body { + frame_width = 800 + + @doc.div({ :class => 'main' }) { + ViewFrame.new("All-time #{@sport_name} Records", + frame_width, @records.all_time).to_html(@doc) + + @records.yearly.each do |year, record| + next if record.empty? + ViewFrame.new("#{year} #{@sport_name} Records", + frame_width, record).to_html(@doc) + end + } + } + end + + end + +end diff --git a/lib/postrunner/TrackView.rb b/lib/postrunner/TrackView.rb index 7171706..fc7bc21 100644 --- a/lib/postrunner/TrackView.rb +++ b/lib/postrunner/TrackView.rb @@ -12,37 +12,35 @@ require 'fit4ruby' -require 'postrunner/ViewWidgets' +require 'postrunner/ViewFrame' module PostRunner class TrackView - include ViewWidgets - def initialize(activity) @activity = activity @session = @activity.fit_activity.sessions[0] @has_geo_data = @session.has_geo_data? end - def head(doc) + def to_html(doc) return unless @has_geo_data - 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) - return unless @has_geo_data + doc.head { + doc.unique(:trackview_style) { + doc.style(style) + doc.link({ 'rel' => 'stylesheet', + 'href' => 'openlayers/theme/default/style.css', + 'type' => 'text/css' }) + doc.script({ 'src' => 'openlayers/OpenLayers.js' }) + } + doc.script(java_script) + } - frame(doc, 'Map') { + ViewFrame.new('Map', 600) { doc.div({ 'id' => 'map', 'class' => 'trackmap' }) - } + }.to_html(doc) end private diff --git a/lib/postrunner/UserProfileView.rb b/lib/postrunner/UserProfileView.rb index 3ff4341..c89216b 100644 --- a/lib/postrunner/UserProfileView.rb +++ b/lib/postrunner/UserProfileView.rb @@ -12,14 +12,12 @@ require 'fit4ruby' -require 'postrunner/ViewWidgets' +require 'postrunner/ViewFrame' module PostRunner class UserProfileView - include ViewWidgets - def initialize(fit_activity, unit_system) @fit_activity = fit_activity @unit_system = unit_system @@ -28,9 +26,7 @@ module PostRunner def to_html(doc) return nil if @fit_activity.user_profiles.empty? - frame(doc, 'User Profile') { - profile.to_html(doc) - } + ViewFrame.new('User Profile', 600, profile).to_html(doc) end def to_s diff --git a/lib/postrunner/View.rb b/lib/postrunner/View.rb new file mode 100644 index 0000000..acfd258 --- /dev/null +++ b/lib/postrunner/View.rb @@ -0,0 +1,97 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = View.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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/HTMLBuilder' +require 'postrunner/ViewTop' +require 'postrunner/ViewBottom' + +module PostRunner + + # Base class for all generated HTML pages. + class View + + attr_reader :doc + + # Create a new View object. + # @param title [String] The title of the HTML page + # @param views [ViewButtons] List of all cross referenced View objects + # @param pages [PagingButtons] List of all pages of this View + def initialize(title, views, pages) + @doc = HTMLBuilder.new(title) + @views = views + @pages = pages + + @doc.unique(:view_style) { + style + } + end + + # Create the body section of the HTML document. + def body + ViewTop.new(@views, @pages).to_html(@doc) + yield if block_given? + ViewBottom.new.to_html(@doc) + + self + end + + # Convert the View into an HTML document. + def to_html + @doc.to_html + end + + # Write the HTML document to a file + # @param file_name [String] Name of the file to write + def write(file_name) + begin + File.write(file_name, to_html) + rescue IOError + Log.fatal "Cannot write file '#{file_name}: #{$!}" + end + end + + private + + def style + @doc.head { + @doc.style(<<"EOT" +body { + font-family: verdana,arial,sans-serif; + margin: 0px; +} +.flexitable { + width: 100%; + border: 2px solid #545454; + border-collapse: collapse; + font-size:11pt; +} +.ft_head_row { + background-color: #DEDEDE +} +.ft_even_row { + background-color: #FCFCFC +} +.ft_odd_row { + background-color: #F1F1F1 +} +.ft_cell { + border: 1px solid #CCCCCC; + padding: 1px 3px; +} +EOT + ) + } + end + + end + +end diff --git a/lib/postrunner/ViewBottom.rb b/lib/postrunner/ViewBottom.rb new file mode 100644 index 0000000..3812919 --- /dev/null +++ b/lib/postrunner/ViewBottom.rb @@ -0,0 +1,54 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = ViewBottom.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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/HTMLBuilder' +require 'postrunner/version' + +module PostRunner + + # This class generates the footer of a HTML page. + class ViewBottom + + # Generate the HTML code to that describes the foot section. + # @param doc [HTMLBuilder] Reference to the HTML document to add to. + def to_html(doc) + doc.unique(:viewbottom_style) { + doc.head { doc.style(style) } + } + doc.div({ :class => 'footer' }){ + doc.hr + doc.div({ :class => 'copyright' }) { + doc.text("Generated by ") + doc.a('PostRunner', + { :href => 'https://github.com/scrapper/postrunner' }) + doc.text(" #{VERSION} on #{Time.now}") + } + } + end + + private + + def style + < +# +# 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. +# + +module PostRunner + + # This class generates a simple icon menue to select from a set of HTML + # pages (called views). The current page is represented as an inactive icon. + # All other icons are buttons that contain links to other pages. + class ViewButtons + + # Create a ViewButtons object. + # @params views [Array of NavButtonDef] icons and URLs for all pages. + def initialize(views) + if views.empty? + raise ArgumentError.new("'views' must not be empty") + end + @views = views + self.current_page = views[0].url + end + + # Get the URL of the current page + # @return [String] + def current_page + @current_view_url + end + + # Set the the current page. + # @param page_url [String] URL of the current page. This must either be + # nil or a URL in the predefined set. + def current_page=(page_url) + unless page_url + @current_view_url = nil + return + end + + if (current = @views.find { |v| v.url == page_url }) + @current_view_url = current.url + else + raise ArgumentError.new("#{page_url} is not a URL of a known view") + end + end + + # Iterate over all buttons. A NavButtonDef object is passed to the block + # that contains the icon and URL for the button. If no URL is set, the + # button is inactive. + def each + @views.each do |view| + view = view.clone + if @current_view_url == view.url + view.url = nil + end + yield(view) + end + + end + + end + +end diff --git a/lib/postrunner/ViewFrame.rb b/lib/postrunner/ViewFrame.rb new file mode 100644 index 0000000..863ad2a --- /dev/null +++ b/lib/postrunner/ViewFrame.rb @@ -0,0 +1,93 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = ViewFrame.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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. +# + +module PostRunner + + # Creates an HTML frame around the passed object or HTML block. + class ViewFrame + + # Create a ViewFrame object. + # @param title [String] Title/heading of the framed box + # @param width [Fixnum or nil] Width of the frame. Use nil to set no + # width. + # @param content [Any object that respons to to_html] Object to frame + # @param &block [HTMLBuilder actions] + def initialize(title, width = 600, content = nil, &block) + @title = title + @content = content + @block = block + @width = width + end + + # Generate the HTML code for the frame and the enclosing content. + # @param doc [HTMLBuilder] HTML document + def to_html(doc) + doc.unique(:viewframe_style) { + # Add the necessary style sheet snippets to the document head. + doc.head { doc.style(style) } + } + + attr = { 'class' => 'widget_frame' } + attr['style'] = "width: #{@width}px" if @width + doc.div(attr) { + doc.div({ 'class' => 'widget_frame_title' }) { + doc.b(@title) + } + doc.div { + # The @content holds an object that must respond to to_html to + # generate the HTML code. + if @content + if @content.is_a?(Array) + @content.each { |c| c.to_html(doc) } + else + @content.to_html(doc) + end + end + # The block generates HTML code directly + @block.yield if @block + } + } + end + + private + + def style + < +# +# 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/HTMLBuilder' +require 'postrunner/NavButtonRow' + +module PostRunner + + # This class generates the top part of the HTML page. It contains the logo + # and the menu and navigation buttons. + class ViewTop + + # Create a ViewTop object. + # @param views [Array of NavButtonDef] icons and URLs for views + # @param pages [Array of NavButtonDef] Full list of pages of this view. + def initialize(views, pages) + @views = views + @pages = pages + end + + # Generate the HTML code to that describes the top section. + # @param doc [HTMLBuilder] Reference to the HTML document to add to. + def to_html(doc) + doc.unique(:viewtop_style) { + doc.head { doc.style(style) } + } + doc.div({ :class => 'titlebar' }) { + doc.div('PostRunner', { :class => 'title' }) + + page_selector = NavButtonRow.new('right') + @pages.each do |p| + page_selector.addButton(p.icon, p.url) + end + page_selector.to_html(doc) + + view_selector = NavButtonRow.new + @views.each do |v| + view_selector.addButton(v.icon, v.url) + end + view_selector.to_html(doc) + } + end + + private + + def style + < -# -# 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. -# - -module PostRunner - - module ViewWidgets - - def view_widgets_style(doc) - doc.style(< 'widget_frame' }) { - doc.div({ 'class' => 'widget_frame_title' }) { - doc.b(title) - } - doc.div { - yield if block_given? - } - } - end - - def titlebar(doc, first_page = nil, prev_page = nil, home_page = nil, - next_page = nil, last_page = nil) - # The top title bar. - doc.div({ :class => 'titlebar' }) { - doc.div('PostRunner', { :class => 'title' }) - doc.div({ :class => 'navigator' }) { - button(doc, first_page, 'first.png') - button(doc, prev_page, 'back.png') - button(doc, home_page, 'home.png') - button(doc, next_page, 'forward.png') - button(doc, last_page, 'last.png') - } - } - end - - def button(doc, link, icon) - if link - doc.a({ :href => link }) { - doc.img({ :src => "icons/#{icon}", :class => 'active_button' }) - } - else - doc.img({ :src => "icons/#{icon}", :class => 'inactive_button' }) - end - end - - def footer(doc) - doc.div({ :class => 'footer' }){ - doc.hr - doc.div({ :class => 'copyright' }) { - doc.text("Generated by ") - doc.a('PostRunner', - { :href => 'https://github.com/scrapper/postrunner' }) - doc.text(" #{VERSION} on #{Time.now}") - } - } - end - - end - -end - diff --git a/misc/icons/activities.png b/misc/icons/activities.png new file mode 100644 index 0000000..87b1c38 Binary files /dev/null and b/misc/icons/activities.png differ diff --git a/misc/icons/activities.svg b/misc/icons/activities.svg new file mode 100644 index 0000000..010749a --- /dev/null +++ b/misc/icons/activities.svg @@ -0,0 +1,1582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/View_spec.rb b/spec/View_spec.rb new file mode 100644 index 0000000..7a90a01 --- /dev/null +++ b/spec/View_spec.rb @@ -0,0 +1,61 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = View_spec.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# 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/View' +require 'postrunner/ViewButtons' +require 'postrunner/PagingButtons' + +module PostRunner + + describe PostRunner::View do + + before(:all) do + @view_names = %w( activities record ) + delete_files + end + + after(:all) do + delete_files + end + + def delete_files + @view_names.each do |vn| + page_files(vn).each do |pf| + File.delete(pf) if File.exists?(pf) + end + end + end + + def page_files(vn) + 1.upto(vn == 'record' ? 5 : 3).map { |i| "#{vn}-page#{i}.html" } + end + + it 'should generate view files with multiple pages' do + views = ViewButtons.new( + @view_names.map{ |vn| NavButtonDef. + new("#{vn}.png", "#{vn}-page1.html") } + ) + @view_names.each do |vn| + views.current_page = vn + '-page1.html' + pages = PagingButtons.new(page_files(vn)) + page_files(vn).each do |file| + pages.current_page = file + PostRunner::View.new("Test File: #{file}", views, pages).body. + write(file) + File.exists?(file).should be true + end + end + end + + end + +end -- cgit v1.2.3