commit aae0d3c1166e9549d5f8b501e347b60ba024ce31
Author: Sean C. Rhea
Date: Wed Sep 6 23:59:42 2006 +0000
initial import
diff --git a/COPYING b/COPYING
new file mode 100644
index 000000000..f90922eea
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..64b14ecd8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,53 @@
+#
+# $Id: Makefile,v 1.11 2006/09/06 23:23:03 srhea Exp $
+#
+# Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 2 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+CC=gcc
+CFLAGS=-g -W -Wall -Werror -ansi -pedantic
+CXXFLAGS=-g -W -Wall -Werror
+
+all: ptdl ptpk ptunpk cpint subdirs
+.PHONY: all clean subdirs
+
+clean:
+ rm -f *.o ptdl ptpk ptunpk cpint
+ $(MAKE) -wC gui clean
+
+subdirs:
+ @if test -d gui; \
+ then \
+ if ! test -f gui/Makefile; then cd gui; qmake; cd -; fi; \
+ $(MAKE) -wC gui; \
+ fi
+
+cpint: cpint-cmd.o cpint.o pt.o
+ $(CXX) -o $@ $^
+
+ptdl: ptdl.o pt.o
+ $(CC) -o $@ $^
+
+ptunpk: ptunpk.o pt.o
+ $(CC) -o $@ $^
+
+ptpk: ptpk.o pt.o
+ $(CC) -o $@ $^
+
+ptdl.o: pt.h
+ptunpk.o: pt.h
+
diff --git a/compare_rows.pl b/compare_rows.pl
new file mode 100755
index 000000000..6c6684fe4
--- /dev/null
+++ b/compare_rows.pl
@@ -0,0 +1,83 @@
+#!/usr/bin/perl -w
+#
+# $Id: compare_rows.pl,v 1.1 2006/05/12 21:50:51 srhea Exp $
+#
+# Copyright (c) 2006 Russ Cox (rsc@swtch.com) and Sean Rhea (srhea@srhea.net)
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 2 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+use strict;
+
+if ($#ARGV < 1) {
+ print STDERR "usage: compare_rows.pl [column]\n";
+ exit 1;
+}
+
+my $file1 = $ARGV[0];
+my $file2 = $ARGV[1];
+my $start_col = 0;
+my $stop_col = 8;
+if ($#ARGV >= 2) {
+ my $stop_col = $ARGV[2];
+ my $start_col = $stop_col - 1;
+}
+
+open(FILE1, $file1) or die "Couldn't open $file1\n";
+open(FILE2, $file2) or die "Couldn't open $file2\n";
+
+my $row = 0;
+while() {
+ ++$row;
+ $_ =~ s/^\s+//;
+ my @cols1 = split /\s+/, $_;
+ my $row2 = ;
+ if ($row2) {
+ my @cols2 = split /\s+/, $row2;
+ my $col;
+ for ($col = $start_col; $col < $stop_col; ++$col) {
+ if (($cols1[$col] =~ m/[0-9.]+/) && ($cols2[$col] =~ m/[0-9.]+/)) {
+ if ($col == 0) {
+ # Times are sometimes off by a bit; just make sure they're
+ # within a tenth of a percent.
+ if (($cols1[$col] * 0.999 > $cols2[$col])
+ || ($cols1[$col] * 1.001 < $cols2[$col])) {
+ print "$file1: @cols1\n";
+ print "$file2: @cols2\n";
+ }
+ }
+ else {
+ # All other columns should match exactly.
+ if ($cols1[$col] != $cols2[$col]) {
+ print "$file1: @cols1\n";
+ print "$file2: @cols2\n";
+ }
+ }
+ }
+ elsif (!($cols1[$col] eq $cols2[$col])) {
+ print "$file1: @cols1\n";
+ print "$file2: @cols2\n";
+ }
+ }
+ }
+ else {
+ print "$file1 is longer.\n";
+ }
+}
+
+if () {
+ print "$file2 is longer.\n";
+}
+
diff --git a/cpint-cmd.c b/cpint-cmd.c
new file mode 100644
index 000000000..260338b90
--- /dev/null
+++ b/cpint-cmd.c
@@ -0,0 +1,55 @@
+/*
+ * $Id: cpint-cmd.c,v 1.1 2006/08/11 19:53:07 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "cpint.h"
+
+static void
+one_done_cb(const char *longdate)
+{
+ fprintf(stderr, "Compiling data for ride on %s...", longdate);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int i;
+ double *bests;
+ int bestlen;
+ char *dir = ".";
+ if (argc > 1)
+ dir = argv[1];
+ update_cpi_files(dir, one_done_cb);
+ combine_cpi_files(dir, &bests, &bestlen);
+ for (i = 0; i < bestlen; ++i) {
+ if (bests[i] != 0)
+ printf("%6.3f %3.0f\n", i * 0.021, round(bests[i]));
+ }
+ return 0;
+}
+
diff --git a/cpint.c b/cpint.c
new file mode 100644
index 000000000..1f412fd32
--- /dev/null
+++ b/cpint.c
@@ -0,0 +1,251 @@
+/*
+ * $Id: cpint.c,v 1.4 2006/08/11 19:53:07 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "pt.h"
+
+struct point
+{
+ double secs;
+ int watts;
+ struct point *next;
+};
+
+static struct point *head, *tail;
+time_t start_since_epoch;
+static double last_secs;
+static int rec_int_ms = 1.26 * 1000.0;
+
+static int
+secs_to_interval(double secs)
+{
+ if (rec_int_ms == 0) {
+ fprintf(stderr, "Missing recording interval.\n");
+ exit(1);
+ }
+ return (int) round(secs * 1000.0 / rec_int_ms);
+}
+
+static void
+add_point(double secs, double watts)
+{
+ struct point *p = (struct point*) malloc(sizeof(struct point));
+ p->secs = secs;
+ p->watts = watts;
+ p->next = NULL;
+ if (tail) tail->next = p; else head = p;
+ tail = p;
+}
+
+static void
+time_cb(struct tm *time, time_t since_epoch, void *context)
+{
+ double secs;
+ context = NULL;
+ time = NULL;
+ if (start_since_epoch == 0)
+ start_since_epoch = since_epoch;
+ secs = since_epoch - start_since_epoch;
+ /* Be conservative: a sleep interval counts as all zeros. */
+ add_point(secs, 0.0);
+ last_secs = secs;
+}
+
+static void
+data_cb(double secs, double nm, double mph, double watts, double miles,
+ unsigned cad, unsigned hr, unsigned interval, void *context)
+{
+ context = NULL;
+ nm = 0.0; miles = 0.0; cad = 0; hr = 0; interval = 0;
+ /* Be conservative: count NaN's as zeros. */
+ add_point(secs, (mph == -1.0) ? 0.0 : watts);
+ last_secs = secs;
+}
+
+static void
+error_cb(const char *msg, void *context)
+{
+ context = NULL;
+ fprintf(stderr, "%s\n", msg);
+ exit(1);
+}
+
+void
+one_file(FILE *in, FILE *out)
+{
+ double start_secs, prev_secs, dur_secs, avg, sum;
+ int dur_ints, i, total_intervals;
+ double *bests;
+ struct point *p, *q;
+
+ if (head) {
+ p = head;
+ while (p) { q = p; p = p->next; free(q); }
+ head = tail = NULL;
+ }
+ start_since_epoch = 0;
+ last_secs = 0.0;
+
+ pt_read_raw(in, 0 /* not compat */, NULL, NULL,
+ &time_cb, &data_cb, &error_cb);
+
+ total_intervals = secs_to_interval(tail->secs);
+ bests = (double*) calloc(total_intervals + 1, sizeof(double));
+
+ start_secs = prev_secs = 0.0;
+ for (p = head; p; p = p->next) {
+ sum = 0.0;
+ for (q = p; q; q = q->next) {
+ sum += (q->secs - prev_secs) * q->watts;
+ dur_secs = q->secs - start_secs;
+ dur_ints = secs_to_interval(dur_secs);
+ avg = sum / dur_secs;
+ if (bests[dur_ints] < avg)
+ bests[dur_ints] = avg;
+ prev_secs = q->secs;
+ }
+ start_secs = prev_secs = p->secs;
+ }
+
+ for (i = 0; i <= total_intervals; ++i) {
+ if (bests[i] != 0)
+ fprintf(out, "%6.3f %3.0f\n", i * rec_int_ms / 1000.0 / 60.0,
+ round(bests[i]));
+ }
+}
+
+void
+update_cpi_files(const char *dir, void (*one_done_cb)(const char *date))
+{
+ DIR *dirp;
+ struct dirent *dp;
+ regex_t reg;
+ struct stat sbi, sbo;
+ FILE *in, *out;
+ char *outname;
+ char year[5], mon[3], day[3], hour[3], min[3], sec[3];
+ char longdate[26];
+ struct tm time;
+ time_t t;
+
+ int nmatch = 7;
+ regmatch_t *pmatch = (regmatch_t*) calloc(nmatch, sizeof(regmatch_t));
+
+ if (regcomp(®, "^([0-9][0-9][0-9][0-9])_([0-9][0-9])_([0-9][0-9])"
+ "_([0-9][0-9])_([0-9][0-9])_([0-9][0-9])\\.raw$", REG_EXTENDED))
+ assert(0);
+
+ outname = malloc(strlen(dir) + 25);
+ dirp = opendir(dir);
+ while ((dp = readdir(dirp)) != NULL) {
+ if (regexec(®, dp->d_name, nmatch, pmatch, 0) == 0) {
+ if (stat(dp->d_name, &sbi))
+ assert(0);
+ sprintf(outname, "%s/%s", dir, dp->d_name);
+ strcpy(outname + strlen(outname) - 4, ".cpi");
+ if ((stat(outname, &sbo)) || (sbo.st_mtime < sbi.st_mtime)) {
+ strncpy(year, dp->d_name + pmatch[1].rm_so, 4); year[4] = '\0';
+ strncpy(mon, dp->d_name + pmatch[2].rm_so, 2); mon[2] = '\0';
+ strncpy(day, dp->d_name + pmatch[3].rm_so, 2); day[2] = '\0';
+ strncpy(hour, dp->d_name + pmatch[4].rm_so, 2); hour[2] = '\0';
+ strncpy(min, dp->d_name + pmatch[5].rm_so, 2); min[2] = '\0';
+ strncpy(sec, dp->d_name + pmatch[6].rm_so, 2); sec[2] = '\0';
+ memset(&time, 0, sizeof(time));
+ time.tm_year = atoi(year) - 1900;
+ time.tm_mon = atoi(mon) - 1;
+ time.tm_mday = atoi(day);
+ time.tm_hour = atoi(hour);
+ time.tm_min = atoi(min);
+ time.tm_sec = atoi(sec);
+ time.tm_isdst = -1;
+ t = mktime(&time);
+ assert(t != -1);
+ ctime_r(&t, longdate);
+ longdate[24] = '\0'; /* get rid of newline */
+ one_done_cb(longdate);
+ fflush(stderr);
+ in = fopen(dp->d_name, "r");
+ assert(in);
+ out = fopen(outname, "w");
+ assert(out);
+ one_file(in, out);
+ fclose(in);
+ fclose(out);
+ fprintf(stderr, "done.\n");
+ }
+ }
+ }
+ closedir(dirp);
+ free(pmatch);
+}
+
+void
+combine_cpi_files(const char *dir, double *bests[], int *bestlen)
+{
+ DIR *dirp;
+ struct dirent *dp;
+ FILE *in;
+ char line[40];
+ int lineno;
+ double mins;
+ int watts;
+ double *tmp;
+ int interval;
+
+ *bestlen = 1000;
+
+ *bests = calloc(*bestlen, sizeof(double));
+ dirp = opendir(dir);
+ while ((dp = readdir(dirp)) != NULL) {
+ if (strcmp(".cpi", dp->d_name + dp->d_namlen - 4) == 0) {
+ in = fopen(dp->d_name, "r");
+ assert(in);
+ lineno = 1;
+ while (fgets(line, sizeof(line), in) != NULL) {
+ if (sscanf(line, "%lf %d\n", &mins, &watts) != 2) {
+ fprintf(stderr, "Bad match on line %d: %s", lineno, line);
+ exit(1);
+ }
+ interval = secs_to_interval(mins * 60.0);
+ while (interval >= *bestlen) {
+ tmp = calloc(*bestlen * 2, sizeof(double));
+ memcpy(tmp, *bests, *bestlen * sizeof(double));
+ free(*bests);
+ *bests = tmp;
+ *bestlen *= 2;
+ }
+ if ((*bests)[interval] < watts)
+ (*bests)[interval] = watts;
+ ++lineno;
+ }
+ fclose(in);
+ }
+ }
+ closedir(dirp);
+}
+
+
diff --git a/cpint.h b/cpint.h
new file mode 100644
index 000000000..330412f8e
--- /dev/null
+++ b/cpint.h
@@ -0,0 +1,29 @@
+/*
+ * $Id: cpint.h,v 1.1 2006/08/11 19:53:07 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef __cpint_h
+#define __cpint_h 1
+
+extern void update_cpi_files(const char *dir,
+ void (*one_done_cb)(const char *date));
+extern void combine_cpi_files(const char *dir, double *bests[], int *bestlen);
+
+#endif /* __cpint_h */
+
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 000000000..0514ec7bf
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,42 @@
+
+CONTENT=$(wildcard *.content)
+HTML=$(subst .content,.html,$(CONTENT))
+TARBALLS=$(wildcard gc_*.tgz)
+OTHER=logo.png sample.gp sample.png cpint.gp cpint.png gui-preview.png
+
+all: $(HTML)
+.PHONY: all clean install
+
+clean:
+ rm -f $(HTML)
+
+install:
+ rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) \
+ srhea.net:public_html/goldencheetah/
+
+# ftp -u ftp://srhea@ftp.goldencheetah.org/ $(HTML) $(TARBALLS) $(OTHER)
+
+contact.html: contact.content genpage.pl
+ ./genpage.pl "Contact Us" $< > $@
+
+contrib.html: contrib.content genpage.pl
+ ./genpage.pl "Contributors" $< > $@
+
+download.html: download.content genpage.pl
+ ./genpage.pl "Download" $< > $@
+
+faq.html: faq.content genpage.pl
+ ./genpage.pl "Frequently Asked Questions" $< > $@
+
+index.html: index.content genpage.pl
+ ./genpage.pl "Introduction" $< > $@
+
+license.html: license.content genpage.pl
+ ./genpage.pl "License" $< > $@
+
+search.html: search.content genpage.pl
+ ./genpage.pl "Search" $< > $@
+
+users-guide.html: users-guide.content genpage.pl
+ ./genpage.pl "User's Guide" $< > $@
+
diff --git a/doc/contact.content b/doc/contact.content
new file mode 100644
index 000000000..6cd3d04f7
--- /dev/null
+++ b/doc/contact.content
@@ -0,0 +1,5 @@
+
+
+Please send all correspondence to
+info@goldencheetah.org.
+
diff --git a/doc/contrib.content b/doc/contrib.content
new file mode 100644
index 000000000..939e82fda
--- /dev/null
+++ b/doc/contrib.content
@@ -0,0 +1,18 @@
+
+
+Sean Rhea bought a PowerTap Pro on April 20, 2006, and immediately set to
+figuring out how to use it from his Mac Powerbook without using Virtual PC.
+Within a week, he was able to download the raw data. Shortly thereafter, Russ
+Cox asked what he was up to, and the two worked together to figure out the
+packing format used. By May 4, they could reproduce the numbers given by the
+PowerTap software except for minor discrepancies in the time values. David
+Easter then pointed out how the checksum bytes in the download protocol were
+used, and Sean Rhea coded up their combined discoveries into the two
+utilities, ptdl and ptunpk.
+
+
+Rob Carlsen helped get the serial port version of the PowerTap Pro working
+with the Keyspan USB-to-serial adaptor. Scott Overfield helped me figure out
+that we should be using the /dev/cu.* devices instead of the
+/dev/tty.* ones.
+
diff --git a/doc/cpint.gp b/doc/cpint.gp
new file mode 100644
index 000000000..bb62814a9
--- /dev/null
+++ b/doc/cpint.gp
@@ -0,0 +1,12 @@
+set title "Critical Power Output"
+set xlabel "Interval Duration (MM:SS)"
+set ylabel "Average Power (Watts)"
+set mxtics 5
+set mytics 2
+set yrange [0:]
+set xrange [0.021:120]
+set logscale x
+set xtics ("0:01.26" 0.021, "0:05" 0.08333, "0:12" 0.2, "1:00" 1, "5:00" 5, \
+ "12:00" 12, "30:00" 30, "60:00" 60, "120:00" 120)
+plot 'cpint.out' noti with li lt 1
+pause -1
diff --git a/doc/cpint.png b/doc/cpint.png
new file mode 100644
index 000000000..b65f28c19
Binary files /dev/null and b/doc/cpint.png differ
diff --git a/doc/download.content b/doc/download.content
new file mode 100644
index 000000000..6c42f173c
--- /dev/null
+++ b/doc/download.content
@@ -0,0 +1,43 @@
+
+
+Right now we're only distributing tarballs of the Golden Cheetah source.
+
+
Adds the cpint program for computing critical
+power intervals and the ptpk program for converting from
+PowerTuned data files (see the User's
+Guide).
The first code release, containing ptdl and
+ptunpk.
+
+
+
+
+
+
+We hope to have anonymous CVS access set up soon.
+
diff --git a/doc/faq.content b/doc/faq.content
new file mode 100644
index 000000000..4a292ed8f
--- /dev/null
+++ b/doc/faq.content
@@ -0,0 +1,62 @@
+
+
+
+Why does Golden Cheetah only include command-line utilities? Where's the
+GUI?
+
+
+Because this project is only a hobby of ours, and we haven't had time to
+finish it yet. That said, we are working on it. Here's a teaser screenshot
+to keep you interested:
+
+
+
+
+
+
+Does the output of ptunpk exactly match that of the software
+included with the PowerTap?
+
+
+Almost. If you run it in compatibility mode, using the -c
+option, it matches the PowerTap software's output exactly on everything but
+the time values, at least for the five sample rides we've tried it with.
+The times are a little off, but not by more than 0.1%, so we don't consider it
+a big deal.
+
+
+That said, the PowerTap software does some weird things, like converting from
+kilometers to miles by multiplying by 0.62, but then reporting the miles
+values with five digits after the decimal place. If you run
+ptunpk without the -c option, it will unpack the
+data in the way we think it should. The results mostly match up with the
+official ones, and are almost certainly identical within the range of accuracy
+of the device.
+
+
+I've downloaded and unpacked the data. Now what do I do with it?
+
+
+We highly recommend that you buy and read both Joe Friel's The
+Cyclist's Training Bible and Allen and Coggan's Training and
+Racing with a Power Meter. The former is the definitive book about all
+aspects of cycling training (although we think he's off his rocker in the
+chapter about diet), and the latter is the definitive book about power-based
+training.
+
+
+Also, if you buy either of these books through the links below, we'll get a
+referral fee in return, helping us to support this web site. Thanks!
+
+
+
+
+
+
+
diff --git a/doc/gc_2006-05-16.tgz b/doc/gc_2006-05-16.tgz
new file mode 100644
index 000000000..bc2db38f0
Binary files /dev/null and b/doc/gc_2006-05-16.tgz differ
diff --git a/doc/gc_2006-05-25.tgz b/doc/gc_2006-05-25.tgz
new file mode 100644
index 000000000..eaecfae2c
Binary files /dev/null and b/doc/gc_2006-05-25.tgz differ
diff --git a/doc/gc_2006-05-27.tgz b/doc/gc_2006-05-27.tgz
new file mode 100644
index 000000000..0d25ff19c
Binary files /dev/null and b/doc/gc_2006-05-27.tgz differ
diff --git a/doc/gc_2006-08-11.tgz b/doc/gc_2006-08-11.tgz
new file mode 100644
index 000000000..f04302098
Binary files /dev/null and b/doc/gc_2006-08-11.tgz differ
diff --git a/doc/genpage.pl b/doc/genpage.pl
new file mode 100755
index 000000000..5ed0b9bc7
--- /dev/null
+++ b/doc/genpage.pl
@@ -0,0 +1,131 @@
+#!/usr/bin/perl -w
+#
+# Copyright (c) 2001-2003 Regents of the University of California.
+# All rights reserved.
+#
+# See the file LICENSE included in this distribution for details.
+#
+# $Id: genpage.pl,v 1.3 2006/07/05 16:59:56 srhea Exp $
+
+use strict;
+
+my $title = shift;
+my $content_file = shift;
+open (FILE, "$content_file") or die "Could not open $content_file";
+
+print<
+
+
+
+Golden Cheetah: Power for the Masses
+
+
+
+
+
+In short, we believe that cyclists should be able to download their power data
+to the computer of their choice, analyze it in whatever way they see fit, and
+share their methods of analysis with others.
+
diff --git a/doc/license.content b/doc/license.content
new file mode 100644
index 000000000..29469b01c
--- /dev/null
+++ b/doc/license.content
@@ -0,0 +1,68 @@
+
+
+Golden Cheetah is licensed under the
+GNU General Public License,
+the preamble of which states:
+
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+
+
+For the full text of the license, please click the link above.
+
diff --git a/doc/logo.jpg b/doc/logo.jpg
new file mode 100644
index 000000000..d5947b50c
Binary files /dev/null and b/doc/logo.jpg differ
diff --git a/doc/logo.png b/doc/logo.png
new file mode 100644
index 000000000..0787910f6
Binary files /dev/null and b/doc/logo.png differ
diff --git a/doc/sample.gp b/doc/sample.gp
new file mode 100644
index 000000000..b46ef782b
--- /dev/null
+++ b/doc/sample.gp
@@ -0,0 +1,8 @@
+set title "May 3, 2006 16:24:04"
+set xlabel "Ride Time (minutes)"
+plot \
+ "< awk '/^[^#]/ {print $1, $4}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'Watts' wi li, \
+ "< awk '/^[^#]/ {print $1, $7}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'Heartrate' wi li, \
+ "< awk '/^[^#]/ {print $1, $6}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'Cadence' wi li, \
+ "< awk '/^[^#]/ {print $1, $3}' 2006_05_03_16_24_04.dat | ./smooth.pl 10" ti 'MPH' wi li
+pause -1
diff --git a/doc/sample.png b/doc/sample.png
new file mode 100644
index 000000000..98209d69b
Binary files /dev/null and b/doc/sample.png differ
diff --git a/doc/search.content b/doc/search.content
new file mode 100644
index 000000000..d9222a2f9
--- /dev/null
+++ b/doc/search.content
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
diff --git a/doc/users-guide.content b/doc/users-guide.content
new file mode 100644
index 000000000..dab33ad4f
--- /dev/null
+++ b/doc/users-guide.content
@@ -0,0 +1,222 @@
+
+Currently, Golden Cheetah consists of several command line utilities:
+ptdl, which downloads ride data from a PowerTap Pro version 2.21
+cycling computer, ptunpk, which unpacks the raw bytes downloaded
+by ptdl and outputs more human-friendly ride information, and
+cpint, which computes your critical power (see below). All three
+are written in simple C code but have only been tested on Mac OS X so far.
+We've also written several Perl scripts to help you graph and summarize the
+data.
+
+
+
+Extracting the Data
+
+
+To use ptdl, you'll first need to install
+the drivers for the
+FTDI chip the PowerTap Pro USB Downloader uses. Once these are installed, you
+should be able to just run ptdl without arguments:
+
+
+If everything goes well, ptdl will automatically detect the
+device (/dev/tty.usbserial-3B1 in the example above), read the
+ride data from it, and write to a file named by the date and time at which the
+ride started (2006_05_15_11_34_03.raw in the example; the format
+is YYYY_MM_DD_hh_mm_ss.raw).
+
+
+
+Unpacking the Data
+
+
As shown by the head command above, the data in this
+.raw file is just the raw bytes that represent your ride. To
+unpack those bytes and display them in a more human-friendly format, use
+ptunpk:
+
+
+$ ./ptunpk 2006_05_15_11_34_03.raw
+$ head -5 2006_05_15_11_34_03.dat
+# Time Torq MPH Watts Miles Cad HR Int
+# 2006/5/15 11:34:03 1147707243
+# wheel size=2096 mm, interval=0, rec int=1
+0.021 13.1 2.450 43 0.00781 0 85 0
+0.042 13.4 5.374 97 0.00912 64 85 0
+
+
+ptunpk takes a .raw file for input and writes a
+.dat file as output. Lines that start with an ampersand ("#") in
+this file are comments; the other lines represent measured samples. As shown
+by the first comment in the file, the columns are: time in minutes, torque in
+Newton-meters, speed in miles per hour, power in watts, distance in miles,
+cadence, heart rate, and interval number.
+
+
+
+Summarizing the Data
+
+
+We hope to have a graphical interface to these programs soon, but until then,
+the only summarization tools we have are command-line programs. The script
+intervals.pl summarizes the intervals performed in a workout:
+
+
+
+In the example above, a rider performed five hill intervals, four of which
+climbed a medium size hill that took about 4-5 minutes to climb (intervals
+1, 3, 5, and 9), and one on a shorter hill that took just under 3 minutes to
+climb (interval 7).
+
+
+
+Graphing the Data
+
+
+For graphing the data in the ride, we use smooth.pl and the
+gnuplot program. You can use sample.gp
+to graph the power, heart rate, cadence, and speed for the hill workout above:
+
+
+$ gnuplot sample.gp
+
+
+
+
+
+
+Finding Your "Critical Power"
+
+
+Joe Friel calls the maximum average power a rider can sustain over an interval
+the rider's "critical power" for that duration. The cpint
+program automatically computes your critical power over all interval lengths
+using the data from all your past rides. This program looks at all the
+.raw files in a directory, calculating your maximum power over
+every subinterval length and storing them in a corresponding .cpi
+file. It then combines the data in all of the .cpi files to find
+your critical power over all subintervals of all your rides.
+
+
+$ ls *.raw
+2006_04_28_10_48_33.raw 2006_05_10_17_08_30.raw 2006_05_18_16_32_53.raw
+2006_05_03_16_24_04.raw 2006_05_13_10_29_12.raw 2006_05_21_12_25_07.raw
+2006_05_05_10_52_05.raw 2006_05_15_11_34_03.raw 2006_05_22_18_28_47.raw
+...
+2006_05_09_09_54_29.raw 2006_05_17_16_44_35.raw
+$ ./cpint
+Compiling data for ride on Fri Apr 28 10:48:33 2006...done.
+Compiling data for ride on Sat Apr 29 10:07:48 2006...done.
+Compiling data for ride on Sun Apr 30 14:00:17 2006...done.
+ ...
+Compiling data for ride on Mon May 22 18:28:47 2006...done.
+ 0.021 1264
+ 0.042 1221
+ 0.063 1216
+ ...
+ 5.019 391
+ ...
+171.885 163
+
+
+
+Over this set of rides, the rider's maximum power is 1264 watts, achieved over
+an interval of 0.021 minutes (1.26 seconds). Over all five-minute
+subintervals, he has achieved a maximum average power of 391 watts. The
+longest ride in this set was 171.885 minutes long, and he averaged 163 watts
+over it.
+
+
+We can graph the output of cpint using gnuplot with
+cpint.gp:
+
+
+$ ./cpint > cpint.out
+$ gnuplot cpint.gp
+
+
+
+
+
+The first time you run cpint it will take a while, as it has to
+analyze all your past rides. On subsequent runs, however, it will only
+analyze new files.
+
+
Training and Racing with a Power Meter (see the FAQ) contains a table of critical powers of Cat 5 cyclists
+up through international pros at interval lengths of 5 seconds, 1 minute, 5
+minutes, and 60 minutes. Using this table and the cpint program,
+you can determine whether you're stronger than others in your racing category
+at each interval length and adapt your training program accordingly.
+
+
+
+Converting Old Data
+
+
+
+If you've used the PowerTuned software that comes with the PowerTap you may
+have lots of old ride data in that program that you'd like to include in your
+critical power graph. You can convert the .xml files that
+PowerTuned produces to .raw files using the ptpk
+program:
+
+
+ptpk assumes the input .xml file was generated with
+a wheel size of 2,096 mm and a recording interval of 1. If this is not the
+case, you should specify the correct values with the -w and
+-r options.
+
+
+Note that the PowerTuned software computes the output speed in miles per hour
+by multiplying the measured speed in kilometers per hour by 0.62, and the
+miles per hour values in a .xml file are thus only accurate to
+two significant figures, even though they're printed out to three decimal
+places. Because of this limitation, the sequence ptpk,
+ptunpk is not quite the identity function; in particular, the
+wattage values from ptpk may only be accurate to two significant
+digits.
+
diff --git a/gui/AllPlot.cpp b/gui/AllPlot.cpp
new file mode 100644
index 000000000..ddb3ce539
--- /dev/null
+++ b/gui/AllPlot.cpp
@@ -0,0 +1,176 @@
+/*
+ * $Id: AllPlot.cpp,v 1.2 2006/07/12 02:13:57 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "AllPlot.h"
+#include "RawFile.h"
+#include "Settings.h"
+
+#include
+#include
+#include
+#include
+
+static const inline double
+max(double a, double b) { if (a > b) return a; else return b; }
+
+AllPlot::AllPlot() :
+ hrArray(NULL), wattsArray(NULL), timeArray(NULL), smooth(30)
+{
+ insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
+ setCanvasBackground(Qt::white);
+
+ setAxisTitle(xBottom, "Time (minutes)");
+ setAxisTitle(yLeft, "Power / HR");
+
+ wattsCurve = new QwtPlotCurve("Power");
+ wattsCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
+ wattsCurve->setPen(QPen(Qt::red));
+ wattsCurve->attach(this);
+
+ hrCurve = new QwtPlotCurve("Heart Rate");
+ hrCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
+ hrCurve->setPen(QPen(Qt::blue));
+ hrCurve->attach(this);
+
+ grid = new QwtPlotGrid();
+ grid->enableX(false);
+ QPen gridPen;
+ gridPen.setStyle(Qt::DotLine);
+ grid->setPen(gridPen);
+ grid->attach(this);
+}
+
+struct DataPoint {
+ double time, hr, watts;
+ DataPoint(double t, double h, double w) : time(t), hr(h), watts(w) {}
+};
+
+void
+AllPlot::recalc()
+{
+ int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]);
+ double ymax = 0.0;
+ double totalWatts = 0.0;
+ double totalHr = 0.0;
+ QList list;
+ int i = 0;
+ double *smoothWatts = new double[rideTimeSecs + 1];
+ double *smoothHr = new double[rideTimeSecs + 1];
+ double *smoothTime = new double[rideTimeSecs + 1];
+ for (int secs = 0; secs < smooth; ++secs) {
+ smoothWatts[secs] = 0.0;
+ smoothHr[secs] = 0.0;
+ smoothTime[secs] = secs / 60.0;
+ }
+ for (int secs = smooth; secs <= rideTimeSecs; ++secs) {
+ while ((i < arrayLength) && (timeArray[i] <= secs)) {
+ DataPoint *dp =
+ new DataPoint(timeArray[i], hrArray[i], wattsArray[i]);
+ totalWatts += wattsArray[i];
+ totalHr += hrArray[i];
+ list.append(dp);
+ ++i;
+ }
+ while (!list.empty() && (list.front()->time < secs - smooth)) {
+ DataPoint *dp = list.front();
+ list.removeFirst();
+ totalWatts -= dp->watts;
+ totalHr -= dp->hr;
+ delete dp;
+ }
+ // TODO: this is wrong. We should do a weighted average over the
+ // seconds represented by each point...
+ if (list.empty()) {
+ smoothWatts[secs] = 0.0;
+ smoothHr[secs] = 0.0;
+ }
+ else {
+ smoothWatts[secs] = totalWatts / list.size();
+ if (smoothWatts[secs] > ymax)
+ ymax = smoothWatts[secs];
+ smoothHr[secs] = totalHr / list.size();
+ if (smoothHr[secs] > ymax)
+ ymax = smoothHr[secs];
+ }
+ smoothTime[secs] = secs / 60.0;
+ }
+ wattsCurve->setData(smoothTime, smoothWatts, rideTimeSecs + 1);
+ hrCurve->setData(smoothTime, smoothHr, rideTimeSecs + 1);
+ setAxisScale(xBottom, 0.0, smoothTime[rideTimeSecs]);
+ setAxisScale(yLeft, 0.0, ymax + 30);
+ replot();
+ delete [] smoothWatts;
+ delete [] smoothHr;
+ delete [] smoothTime;
+}
+
+void
+AllPlot::setData(RawFile *raw)
+{
+ delete [] hrArray;
+ delete [] wattsArray;
+ delete [] timeArray;
+ setTitle(raw->startTime.toString(GC_DATETIME_FORMAT));
+ hrArray = new double[raw->points.size()];
+ wattsArray = new double[raw->points.size()];
+ timeArray = new double[raw->points.size()];
+ arrayLength = 0;
+ QListIterator i(raw->points);
+ while (i.hasNext()) {
+ RawFilePoint *point = i.next();
+ timeArray[arrayLength] = point->secs;
+ hrArray[arrayLength] = max(0, point->hr);
+ wattsArray[arrayLength] = max(0, point->watts);
+ ++arrayLength;
+ }
+ recalc();
+}
+
+void
+AllPlot::showPower(int state)
+{
+ assert(state != Qt::PartiallyChecked);
+ wattsCurve->setVisible(state == Qt::Checked);
+ replot();
+}
+
+void
+AllPlot::showHr(int state)
+{
+ assert(state != Qt::PartiallyChecked);
+ hrCurve->setVisible(state == Qt::Checked);
+ replot();
+}
+
+void
+AllPlot::showGrid(int state)
+{
+ assert(state != Qt::PartiallyChecked);
+ grid->setVisible(state == Qt::Checked);
+ replot();
+}
+
+void
+AllPlot::setSmoothing(int value)
+{
+ smooth = value;
+ recalc();
+}
+
diff --git a/gui/AllPlot.h b/gui/AllPlot.h
new file mode 100644
index 000000000..831eb08e3
--- /dev/null
+++ b/gui/AllPlot.h
@@ -0,0 +1,67 @@
+/*
+ * $Id: AllPlot.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _GC_AllPlot_h
+#define _GC_AllPlot_h 1
+
+#include
+
+class QwtPlotCurve;
+class QwtPlotGrid;
+class RawFile;
+
+class AllPlot : public QwtPlot
+{
+ Q_OBJECT
+
+ public:
+
+ QwtPlotCurve *wattsCurve;
+ QwtPlotCurve *hrCurve;
+
+ AllPlot();
+
+ int smoothing() const { return smooth; }
+
+ void setData(RawFile *raw);
+
+ public slots:
+
+ void showPower(int state);
+ void showHr(int state);
+ void showGrid(int state);
+ void setSmoothing(int value);
+
+ protected:
+
+ QwtPlotGrid *grid;
+
+ double *hrArray;
+ double *wattsArray;
+ double *timeArray;
+ int arrayLength;
+
+ int smooth;
+
+ void recalc();
+};
+
+#endif // _GC_AllPlot_h
+
diff --git a/gui/ChooseCyclistDialog.cpp b/gui/ChooseCyclistDialog.cpp
new file mode 100644
index 000000000..194d32e1d
--- /dev/null
+++ b/gui/ChooseCyclistDialog.cpp
@@ -0,0 +1,112 @@
+/*
+ * $Id: ChooseCyclistDialog.cpp,v 1.3 2006/07/04 12:55:40 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "ChooseCyclistDialog.h"
+#include
+
+ChooseCyclistDialog::ChooseCyclistDialog(const QDir &home, bool allowNew) :
+ home(home)
+{
+ setWindowTitle("Choose a Cyclist");
+
+ listWidget = new QListWidget(this);
+ listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ QStringListIterator i(home.entryList(QDir::Dirs | QDir::NoDotAndDotDot));
+ while (i.hasNext()) {
+ QString name = i.next();
+ new QListWidgetItem(name, listWidget);
+ }
+
+ if (allowNew)
+ newButton = new QPushButton(tr("&New..."), this);
+ okButton = new QPushButton(tr("&Open"), this);
+ cancelButton = new QPushButton(tr("&Cancel"), this);
+
+ okButton->setEnabled(false);
+
+ connect(okButton, SIGNAL(clicked()), this, SLOT(accept()));
+ if (allowNew)
+ connect(newButton, SIGNAL(clicked()), this, SLOT(newClicked()));
+ connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
+ connect(listWidget,
+ SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
+ this, SLOT(enableOk(QListWidgetItem*)));
+ connect(listWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)),
+ this, SLOT(accept()));
+
+ QHBoxLayout *buttonLayout = new QHBoxLayout;
+ buttonLayout->addWidget(okButton);
+ if (allowNew)
+ buttonLayout->addWidget(newButton);
+ buttonLayout->addWidget(cancelButton);
+
+ QVBoxLayout *mainLayout = new QVBoxLayout(this);
+ mainLayout->addWidget(listWidget);
+ mainLayout->addLayout(buttonLayout);
+}
+
+QString
+ChooseCyclistDialog::choice()
+{
+ return listWidget->currentItem()->text();
+}
+
+void
+ChooseCyclistDialog::enableOk(QListWidgetItem *item)
+{
+ okButton->setEnabled(item != NULL);
+}
+
+void
+ChooseCyclistDialog::cancelClicked()
+{
+ reject();
+}
+
+QString
+ChooseCyclistDialog::newCyclistDialog(QDir &homeDir, QWidget *parent)
+{
+ QDir home(homeDir);
+ bool ok;
+ QString name = QInputDialog::getText(parent, tr("Create New Cyclist"),
+ tr("Enter New Cyclist's Name"),
+ QLineEdit::Normal, "", &ok);
+ if (ok && !name.isEmpty()) {
+ fprintf(stderr, "New name: %s\n", name.toAscii().data());
+ if (!home.exists(name)) {
+ if (home.mkdir(name))
+ return name;
+ QMessageBox::critical(0, tr("Fatal Error"),
+ tr("Can't create new directory ")
+ + home.path() + "/" + name, "OK");
+ }
+ }
+ return QString();
+}
+
+void
+ChooseCyclistDialog::newClicked()
+{
+ QString name = newCyclistDialog(home, this);
+ if (!name.isEmpty())
+ new QListWidgetItem(name, listWidget);
+}
+
diff --git a/gui/ChooseCyclistDialog.h b/gui/ChooseCyclistDialog.h
new file mode 100644
index 000000000..900c0c1e9
--- /dev/null
+++ b/gui/ChooseCyclistDialog.h
@@ -0,0 +1,53 @@
+/*
+ * $Id: ChooseCyclistDialog.h,v 1.3 2006/07/04 12:55:40 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _GC_ChooseCyclistDialog_h
+#define _GC_ChooseCyclistDialog_h 1
+
+#include
+#include
+
+class QListWidget;
+class QListWidgetItem;
+class QPushButton;
+
+class ChooseCyclistDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ ChooseCyclistDialog(const QDir &home, bool allowNew);
+ QString choice();
+ static QString newCyclistDialog(QDir &homeDir, QWidget *parent);
+
+ private slots:
+ void newClicked();
+ void cancelClicked();
+ void enableOk(QListWidgetItem *item);
+
+ private:
+
+ QDir home;
+ QListWidget *listWidget;
+ QPushButton *okButton, *newButton, *cancelButton;
+};
+
+#endif // _GC_ChooseCyclistDialog_h
+
diff --git a/gui/DownloadRideDialog.cpp b/gui/DownloadRideDialog.cpp
new file mode 100644
index 000000000..96640e131
--- /dev/null
+++ b/gui/DownloadRideDialog.cpp
@@ -0,0 +1,312 @@
+/*
+ * $Id: DownloadRideDialog.cpp,v 1.4 2006/08/11 20:02:13 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "DownloadRideDialog.h"
+#include "MainWindow.h"
+#include
+#include
+#include
+
+#define MAX_DEVICES 10
+
+DownloadRideDialog::DownloadRideDialog(MainWindow *mainWindow,
+ const QDir &home) :
+ mainWindow(mainWindow), home(home), fd(-1), out(NULL), device(NULL),
+ notifier(NULL), timer(NULL), blockCount(0)
+{
+ setAttribute(Qt::WA_DeleteOnClose);
+ setWindowTitle("Download Ride Data");
+
+ listWidget = new QListWidget(this);
+ listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ QLabel *availLabel = new QLabel(tr("Available devices:"), this);
+ QLabel *instructLabel = new QLabel(tr("Instructions:"), this);
+ label = new QLabel(this);
+ label->setIndent(10);
+
+ downloadButton = new QPushButton(tr("&Download"), this);
+ rescanButton = new QPushButton(tr("&Rescan"), this);
+ cancelButton = new QPushButton(tr("&Cancel"), this);
+
+ connect(downloadButton, SIGNAL(clicked()), this, SLOT(downloadClicked()));
+ connect(rescanButton, SIGNAL(clicked()), this, SLOT(scanDevices()));
+ connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
+ connect(listWidget,
+ SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
+ this, SLOT(setReadyInstruct()));
+ connect(listWidget, SIGNAL(itemDoubleClicked(QListWidgetItem*)),
+ this, SLOT(downloadClicked()));
+
+ QHBoxLayout *buttonLayout = new QHBoxLayout;
+ buttonLayout->addWidget(downloadButton);
+ buttonLayout->addWidget(rescanButton);
+ buttonLayout->addWidget(cancelButton);
+
+ QVBoxLayout *mainLayout = new QVBoxLayout(this);
+ mainLayout->addWidget(availLabel);
+ mainLayout->addWidget(listWidget);
+ mainLayout->addWidget(instructLabel);
+ mainLayout->addWidget(label);
+ mainLayout->addLayout(buttonLayout);
+
+ scanDevices();
+}
+
+DownloadRideDialog::~DownloadRideDialog()
+{
+ if (device)
+ free(device);
+ if (fd >= 0)
+ ::close(fd);
+ if (out)
+ fclose(out);
+}
+
+void
+DownloadRideDialog::setReadyInstruct()
+{
+ label->setText(tr("Make sure the PowerTap unit is turned on,\n"
+ "and that the screen display says, \"Host\",\n"
+ "then click Download to begin downloading."));
+}
+
+void
+DownloadRideDialog::scanDevices()
+{
+ listWidget->clear();
+ char *devices[MAX_DEVICES];
+ int devcnt = pt_find_device(devices, MAX_DEVICES);
+ for (int i = 0; i < devcnt; ++i) {
+ new QListWidgetItem(devices[i], listWidget);
+ free(devices[i]);
+ }
+ if (listWidget->count() == 1) {
+ listWidget->setCurrentRow(0);
+ setReadyInstruct();
+ downloadButton->setEnabled(true);
+ downloadButton->setFocus();
+ }
+ else {
+ downloadButton->setEnabled(false);
+ if (listWidget->count() > 1) {
+ label->setText(tr("Select the device from the above list from\n"
+ "which you would like to download a ride."));
+ }
+ else {
+ label->setText(tr("No devices found. Make sure the PowerTap\n"
+ "unit is plugged into the computer's USB port,\n"
+ "then click \"Rescan\" to check again."));
+ }
+ }
+}
+
+static void
+time_cb(struct tm *time, void *self)
+{
+ ((DownloadRideDialog*) self)->time_cb(time);
+}
+
+static void
+record_cb(unsigned char *buf, void *self)
+{
+ ((DownloadRideDialog*) self)->record_cb(buf);
+}
+
+void
+DownloadRideDialog::time_cb(struct tm *time)
+{
+ timer->stop();
+ if (!out) {
+ if (!time) {
+ QMessageBox::critical(this, tr("Read error"),
+ tr("Can't find ride time"));
+ reject();
+ }
+ sprintf(outname, "%04d_%02d_%02d_%02d_%02d_%02d.raw",
+ time->tm_year + 1900, time->tm_mon + 1, time->tm_mday,
+ time->tm_hour, time->tm_min, time->tm_sec);
+ assert(strlen(outname) == sizeof(outname) - 1);
+ label->setText(label->text() + tr("done.\nWriting to ")
+ + outname + ".");
+ std::string path = home.absolutePath().toStdString() + "/" + outname;
+ if ((out = fopen(path.c_str(), "r")) != NULL) {
+ if (QMessageBox::warning(this,
+ tr("Ride Already Downloaded"),
+ tr("This ride appears to have already ")
+ + tr("been downloaded. Do you want to ")
+ + tr("download it again and overwrite ")
+ + tr("the previous download?"),
+ tr("&Overwrite"), tr("&Cancel"),
+ QString(), 1, 1) == 1) {
+ reject();
+ }
+ }
+ if ((out = fopen(path.c_str(), "w")) == NULL) {
+ QMessageBox::critical(this, tr("Write error"),
+ tr("Can't open ") + path.c_str()
+ + tr(" for writing: ") + strerror(errno));
+ reject();
+ }
+ label->setText(label->text() + tr("\nReading ride data..."));
+ }
+ timer->start(5000);
+}
+
+void
+DownloadRideDialog::record_cb(unsigned char *buf)
+{
+ timer->stop();
+ if (!out) {
+ QMessageBox::critical(this, tr("Read error"),
+ tr("Can't find ride time."));
+ reject();
+ }
+ for (int i = 0; i < 6; ++i)
+ fprintf(out, "%02x%s", buf[i], (i == 5) ? "\n" : " ");
+ if ((++blockCount % 256) == 0) {
+ label->setText(label->text() + ".");
+ }
+ timer->start(5000);
+}
+
+void
+DownloadRideDialog::readVersion()
+{
+ if (notifier)
+ notifier->setEnabled(false);
+ int r = pt_read_version(&vstate, fd, hwecho);
+ if (r == PT_DONE) {
+ if (notifier) {
+ delete notifier;
+ notifier = NULL;
+ }
+ if (timer) {
+ delete timer;
+ timer = NULL;
+ }
+ label->setText(label->text() + tr("done."));
+ ::close(fd);
+ fd = open(device, O_RDWR | O_NOCTTY);
+ if (fd < 0) {
+ QMessageBox::critical(this, tr("Read error"),
+ tr("Could not open device, ") + device +
+ + ": " + strerror(errno));
+ reject();
+ }
+ pt_make_async(fd);
+ label->setText(label->text() + tr("\nReading ride time..."));
+ memset(&dstate, 0, sizeof(dstate));
+ readData();
+ }
+ else {
+ assert(r == PT_NEED_READ);
+ if (notifier)
+ notifier->setEnabled(true);
+ else {
+ notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
+ connect(notifier, SIGNAL(activated(int)), this, SLOT(readVersion()));
+ }
+ if (!timer) {
+ timer = new QTimer(this);
+ connect(timer, SIGNAL(timeout()), this, SLOT(versionTimeout()));
+ timer->start(5000);
+ }
+ }
+}
+
+void
+DownloadRideDialog::versionTimeout()
+{
+ timer->stop();
+ label->setText(label->text() + tr("timeout."));
+ QMessageBox::critical(this, tr("Read Error"),
+ tr("Timed out reading device ") + device
+ + tr(". Check that the device is plugged in and ")
+ + tr("try again."));
+ reject();
+}
+
+void
+DownloadRideDialog::readData()
+{
+ if (notifier)
+ notifier->setEnabled(false);
+ int r = pt_read_data(&dstate, fd, hwecho, ::time_cb, ::record_cb, this);
+ if (r == PT_DONE) {
+ if (notifier) {
+ delete notifier;
+ notifier = NULL;
+ }
+ if (timer) {
+ delete timer;
+ timer = NULL;
+ }
+ label->setText(label->text() + tr("done."));
+ QMessageBox::information(this, tr("Success"), tr("Download complete."));
+ fclose(out);
+ out = NULL;
+ mainWindow->addRide(outname);
+ accept();
+ }
+ else {
+ assert(r == PT_NEED_READ);
+ if (notifier)
+ notifier->setEnabled(true);
+ else {
+ notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
+ connect(notifier, SIGNAL(activated(int)), this, SLOT(readData()));
+ }
+ if (!timer) {
+ timer = new QTimer(this);
+ connect(timer, SIGNAL(timeout()), this, SLOT(versionTimeout()));
+ timer->start(5000);
+ }
+ }
+}
+
+void
+DownloadRideDialog::downloadClicked()
+{
+ downloadButton->setEnabled(false);
+ rescanButton->setEnabled(false);
+ if (device)
+ free(device);
+ device = strdup(listWidget->currentItem()->text().toAscii().data());
+ hwecho = pt_hwecho(device);
+ fd = open(device, O_RDWR | O_NOCTTY);
+ if (fd < 0) {
+ QMessageBox::critical(this, tr("Read error"),
+ tr("Could not open device, ") + device +
+ + ": " + strerror(errno));
+ reject();
+ }
+ pt_make_async(fd);
+ label->setText(tr("Reading version information..."));
+ memset(&vstate, 0, sizeof(vstate));
+ readVersion();
+}
+
+void
+DownloadRideDialog::cancelClicked()
+{
+ reject();
+}
+
diff --git a/gui/DownloadRideDialog.h b/gui/DownloadRideDialog.h
new file mode 100644
index 000000000..3b8d892a5
--- /dev/null
+++ b/gui/DownloadRideDialog.h
@@ -0,0 +1,72 @@
+/*
+ * $Id: DownloadRideDialog.h,v 1.4 2006/08/11 20:02:13 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _GC_DownloadRideDialog_h
+#define _GC_DownloadRideDialog_h 1
+
+#include
+extern "C" {
+#include "../pt.h"
+}
+
+class MainWindow;
+
+class DownloadRideDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ DownloadRideDialog(MainWindow *mainWindow, const QDir &home);
+ ~DownloadRideDialog();
+
+ void time_cb(struct tm *time);
+ void record_cb(unsigned char *buf);
+
+ private slots:
+ void downloadClicked();
+ void cancelClicked();
+ void setReadyInstruct();
+ void scanDevices();
+ void readVersion();
+ void readData();
+ void versionTimeout();
+
+ private:
+
+ MainWindow *mainWindow;
+ QDir home;
+ QListWidget *listWidget;
+ QPushButton *downloadButton, *rescanButton, *cancelButton;
+ QLabel *label;
+ int fd;
+ FILE *out;
+ char outname[24];
+
+ char *device;
+ struct pt_read_version_state vstate;
+ struct pt_read_data_state dstate;
+ QSocketNotifier *notifier;
+ QTimer *timer;
+ int blockCount;
+ int hwecho;
+};
+
+#endif // _GC_DownloadRideDialog_h
+
diff --git a/gui/GoldenCheetah.pro b/gui/GoldenCheetah.pro
new file mode 100644
index 000000000..408976efc
--- /dev/null
+++ b/gui/GoldenCheetah.pro
@@ -0,0 +1,30 @@
+######################################################################
+# Automatically generated by qmake (2.00a) Sun May 28 19:43:48 2006
+######################################################################
+
+TEMPLATE = app
+TARGET +=
+DEPENDPATH += .
+INCLUDEPATH += ../qwt-20060130/include
+CONFIG += static
+OBJECTS += ../pt.o
+LIBS += /usr/local/lib/libqwt.a
+LIBS += -lz -framework Carbon
+
+# Input
+HEADERS += \
+ AllPlot.h \
+ ChooseCyclistDialog.h \
+ DownloadRideDialog.h \
+ MainWindow.h \
+ RawFile.h \
+ RideItem.h
+SOURCES += \
+ AllPlot.cpp \
+ ChooseCyclistDialog.cpp \
+ DownloadRideDialog.cpp \
+ MainWindow.cpp \
+ RawFile.cpp \
+ RideItem.cpp \
+ \
+ main.cpp
diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp
new file mode 100644
index 000000000..0affa5e92
--- /dev/null
+++ b/gui/MainWindow.cpp
@@ -0,0 +1,279 @@
+/*
+ * $Id: MainWindow.cpp,v 1.7 2006/07/12 02:13:57 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "MainWindow.h"
+#include "AllPlot.h"
+#include "ChooseCyclistDialog.h"
+#include "DownloadRideDialog.h"
+#include "RawFile.h"
+#include "RideItem.h"
+#include "Settings.h"
+#include
+#include
+#include
+
+#define FOLDER_TYPE 0
+#define RIDE_TYPE 1
+
+static char *rideFileRegExp = ("^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)"
+ "_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.raw$");
+
+MainWindow::MainWindow(const QDir &home) :
+ home(home), settings(GC_SETTINGS_CO, GC_SETTINGS_APP)
+{
+ setWindowTitle(home.dirName());
+ settings.setValue(GC_SETTINGS_LAST, home.dirName());
+
+ QVariant geom = settings.value(GC_SETTINGS_MAIN_GEOM);
+ if (geom == QVariant())
+ resize(640, 480);
+ else
+ setGeometry(geom.toRect());
+
+ splitter = new QSplitter(this);
+ setCentralWidget(splitter);
+
+ treeWidget = new QTreeWidget;
+ treeWidget->setColumnCount(3);
+ treeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+ // TODO:
+ // treeWidget->header()->resizeSection(0,80);
+ treeWidget->header()->hide();
+
+ allRides = new QTreeWidgetItem(treeWidget, FOLDER_TYPE);
+ allRides->setText(0, tr("All Rides"));
+ treeWidget->expandItem(allRides);
+ splitter->addWidget(treeWidget);
+
+ QRegExp rx(rideFileRegExp);
+ QStringList filters;
+ filters << "*.raw";
+ QTreeWidgetItem *last = NULL;
+ QStringListIterator i(home.entryList(filters, QDir::Files));
+ while (i.hasNext()) {
+ QString name = i.next();
+ if (rx.exactMatch(name)) {
+ assert(rx.numCaptures() == 6);
+ QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt());
+ QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt());
+ QDateTime dt(date, time);
+ last = new RideItem(allRides, RIDE_TYPE, home.path(), name, dt);
+ }
+ }
+
+ tabWidget = new QTabWidget;
+ rideSummary = new QTextEdit;
+ rideSummary->setReadOnly(true);
+ tabWidget->addTab(rideSummary, "Ride Summary");
+
+ QWidget *window = new QWidget;
+ QVBoxLayout *vlayout = new QVBoxLayout;
+
+ QHBoxLayout *showLayout = new QHBoxLayout;
+ QLabel *showLabel = new QLabel("Show:", this);
+ showLayout->addWidget(showLabel);
+
+ QCheckBox *showGrid = new QCheckBox("Grid", this);
+ showGrid->setCheckState(Qt::Checked);
+ showLayout->addWidget(showGrid);
+
+ QCheckBox *showPower = new QCheckBox("Power", this);
+ showPower->setCheckState(Qt::Checked);
+ showLayout->addWidget(showPower);
+
+ QCheckBox *showHr = new QCheckBox("Heart Rate", this);
+ showHr->setCheckState(Qt::Checked);
+ showLayout->addWidget(showHr);
+
+ QHBoxLayout *smoothLayout = new QHBoxLayout;
+ QLabel *smoothLabel = new QLabel(tr("Smoothing (secs)"), this);
+ smoothLineEdit = new QLineEdit(this);
+ smoothLineEdit->setFixedWidth(30);
+
+ smoothLayout->addWidget(smoothLabel);
+ smoothLayout->addWidget(smoothLineEdit);
+ smoothSlider = new QSlider(Qt::Horizontal);
+ smoothSlider->setTickPosition(QSlider::TicksBelow);
+ smoothSlider->setTickInterval(1);
+ smoothSlider->setMinimum(2);
+ smoothSlider->setMaximum(60);
+ smoothLineEdit->setValidator(new QIntValidator(smoothSlider->minimum(),
+ smoothSlider->maximum(),
+ smoothLineEdit));
+ smoothLayout->addWidget(smoothSlider);
+ allPlot = new AllPlot;
+ smoothSlider->setValue(allPlot->smoothing());
+ smoothLineEdit->setText(QString("%1").arg(allPlot->smoothing()));
+ vlayout->addWidget(allPlot);
+ vlayout->addLayout(showLayout);
+ vlayout->addLayout(smoothLayout);
+ window->setLayout(vlayout);
+ window->show();
+
+ tabWidget->addTab(window, "All-in-One Graph");
+ splitter->addWidget(tabWidget);
+
+ QVariant splitterSizes = settings.value(GC_SETTINGS_SPLITTER_SIZES);
+ if (splitterSizes != QVariant())
+ splitter->restoreState(splitterSizes.toByteArray());
+ else {
+ QList sizes;
+ sizes.append(250);
+ sizes.append(390);
+ splitter->setSizes(sizes);
+ }
+
+ connect(treeWidget, SIGNAL(itemSelectionChanged()),
+ this, SLOT(rideSelected()));
+ connect(splitter, SIGNAL(splitterMoved(int,int)),
+ this, SLOT(splitterMoved()));
+ connect(showPower, SIGNAL(stateChanged(int)),
+ allPlot, SLOT(showPower(int)));
+ connect(showHr, SIGNAL(stateChanged(int)),
+ allPlot, SLOT(showHr(int)));
+ connect(showGrid, SIGNAL(stateChanged(int)),
+ allPlot, SLOT(showGrid(int)));
+ connect(smoothSlider, SIGNAL(valueChanged(int)),
+ this, SLOT(setSmoothingFromSlider()));
+ connect(smoothLineEdit, SIGNAL(returnPressed()),
+ this, SLOT(setSmoothingFromLineEdit()));
+
+ QMenu *fileMenu = new QMenu(tr("&File"), this);
+ fileMenu->addAction(tr("&New..."), this,
+ SLOT(newCyclist()), tr("Ctrl+N"));
+ fileMenu->addAction(tr("&Open..."), this,
+ SLOT(openCyclist()), tr("Ctrl+O"));
+ fileMenu->addAction(tr("&Download ride..."), this,
+ SLOT(downloadRide()), tr("Ctrl+D"));
+
+ QMenuBar *menuBar = new QMenuBar(this);
+ menuBar->addMenu(fileMenu);
+
+ if (last != NULL) {
+ treeWidget->setCurrentItem(last);
+ rideSelected();
+ }
+}
+
+void
+MainWindow::addRide(QString name)
+{
+ QRegExp rx(rideFileRegExp);
+ if (!rx.exactMatch(name))
+ assert(false);
+ assert(rx.numCaptures() == 6);
+ QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt());
+ QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt());
+ QDateTime dt(date, time);
+ RideItem *last = new RideItem(allRides, RIDE_TYPE, home.path(), name, dt);
+ treeWidget->setCurrentItem(last);
+}
+
+void
+MainWindow::newCyclist()
+{
+ QDir newHome = home;
+ newHome.cdUp();
+ QString name = ChooseCyclistDialog::newCyclistDialog(newHome, this);
+ if (!name.isEmpty()) {
+ newHome.cd(name);
+ if (!newHome.exists())
+ assert(false);
+ MainWindow *main = new MainWindow(newHome);
+ main->show();
+ }
+}
+
+void
+MainWindow::openCyclist()
+{
+ QDir newHome = home;
+ newHome.cdUp();
+ ChooseCyclistDialog d(newHome, false);
+ d.setModal(true);
+ if (d.exec() == QDialog::Accepted) {
+ newHome.cd(d.choice());
+ if (!newHome.exists())
+ assert(false);
+ MainWindow *main = new MainWindow(newHome);
+ main->show();
+ }
+}
+
+void
+MainWindow::downloadRide()
+{
+ (new DownloadRideDialog(this, home))->show();
+}
+
+void
+MainWindow::rideSelected()
+{
+ assert(treeWidget->selectedItems().size() <= 1);
+ if (treeWidget->selectedItems().size() == 1) {
+ QTreeWidgetItem *which = treeWidget->selectedItems().first();
+ if (which->type() == RIDE_TYPE) {
+ RideItem *ride = (RideItem*) which;
+ rideSummary->setHtml(ride->htmlSummary());
+ rideSummary->setAlignment(Qt::AlignCenter);
+ allPlot->setData(ride->raw);
+ return;
+ }
+ }
+ rideSummary->clear();
+}
+
+void
+MainWindow::resizeEvent(QResizeEvent*)
+{
+ settings.setValue(GC_SETTINGS_MAIN_GEOM, geometry());
+}
+
+void
+MainWindow::moveEvent(QMoveEvent*)
+{
+ settings.setValue(GC_SETTINGS_MAIN_GEOM, geometry());
+}
+
+void
+MainWindow::splitterMoved()
+{
+ settings.setValue(GC_SETTINGS_SPLITTER_SIZES, splitter->saveState());
+}
+
+void
+MainWindow::setSmoothingFromSlider()
+{
+ if (allPlot->smoothing() != smoothSlider->value()) {
+ allPlot->setSmoothing(smoothSlider->value());
+ smoothLineEdit->setText(QString("%1").arg(allPlot->smoothing()));
+ }
+}
+
+void
+MainWindow::setSmoothingFromLineEdit()
+{
+ int value = smoothLineEdit->text().toInt();
+ if (value != allPlot->smoothing()) {
+ allPlot->setSmoothing(value);
+ smoothSlider->setValue(value);
+ }
+}
+
diff --git a/gui/MainWindow.h b/gui/MainWindow.h
new file mode 100644
index 000000000..d408f7cb7
--- /dev/null
+++ b/gui/MainWindow.h
@@ -0,0 +1,66 @@
+/*
+ * $Id: MainWindow.h,v 1.6 2006/07/11 21:20:21 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _GC_MainWindow_h
+#define _GC_MainWindow_h 1
+
+#include
+#include
+
+class AllPlot;
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+ public:
+ MainWindow(const QDir &home);
+ void addRide(QString name);
+
+ protected:
+ virtual void resizeEvent(QResizeEvent*);
+ virtual void moveEvent(QMoveEvent*);
+
+ private slots:
+ void rideSelected();
+ void splitterMoved();
+ void newCyclist();
+ void openCyclist();
+ void downloadRide();
+ void setSmoothingFromSlider();
+ void setSmoothingFromLineEdit();
+
+ private:
+
+ QDir home;
+ QSettings settings;
+
+ QSplitter *splitter;
+ QTreeWidget *treeWidget;
+ QTabWidget *tabWidget;
+ QTextEdit *rideSummary;
+ AllPlot *allPlot;
+ QSlider *smoothSlider;
+ QLineEdit *smoothLineEdit;
+ QTreeWidgetItem *allRides;
+};
+
+#endif // _GC_MainWindow_h
+
diff --git a/gui/RawFile.cpp b/gui/RawFile.cpp
new file mode 100644
index 000000000..b03d077ad
--- /dev/null
+++ b/gui/RawFile.cpp
@@ -0,0 +1,100 @@
+/*
+ * $Id: RawFile.cpp,v 1.2 2006/08/11 19:58:07 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "RawFile.h"
+#include
+extern "C" {
+#include "../pt.h"
+}
+
+struct RawFileReadState
+{
+ QList &points;
+ QStringList &errors;
+ double last_secs, last_miles;
+ unsigned last_interval;
+ time_t start_since_epoch;
+ unsigned rec_int;
+ RawFileReadState(QList &points, QStringList &errors) :
+ points(points), errors(errors), last_secs(0.0), last_miles(0.0),
+ last_interval(0), start_since_epoch(0), rec_int(0) {}
+};
+
+static void
+config_cb(unsigned /*interval*/, unsigned rec_int,
+ unsigned /*wheel_sz_mm*/, void *context)
+{
+ RawFileReadState *state = (RawFileReadState*) context;
+ // Assume once set, rec_int should never change.
+ assert((state->rec_int == 0) || (state->rec_int == rec_int));
+ state->rec_int = rec_int;
+}
+
+static void
+time_cb(struct tm *, time_t since_epoch, void *context)
+{
+ RawFileReadState *state = (RawFileReadState*) context;
+ if (state->start_since_epoch == 0)
+ state->start_since_epoch = since_epoch;
+ double secs = since_epoch - state->start_since_epoch;
+ RawFilePoint *point = new RawFilePoint(secs, -1.0, -1.0, -1.0,
+ state->last_miles, 0, 0,
+ state->last_interval);
+ state->points.append(point);
+ state->last_secs = secs;
+}
+
+static void
+data_cb(double secs, double nm, double mph, double watts, double miles,
+ unsigned cad, unsigned hr, unsigned interval, void *context)
+{
+ RawFileReadState *state = (RawFileReadState*) context;
+ RawFilePoint *point = new RawFilePoint(secs, nm, mph, watts, miles,
+ cad, hr, interval);
+ state->points.append(point);
+ state->last_secs = secs;
+ state->last_miles = miles;
+ state->last_interval = interval;
+}
+
+static void
+error_cb(const char *msg, void *context)
+{
+ RawFileReadState *state = (RawFileReadState*) context;
+ state->errors.append(QString(msg));
+}
+
+RawFile *RawFile::readFile(const QFile &file, QStringList &errors)
+{
+ RawFile *result = new RawFile(file.fileName());
+ if (!result->file.open(QIODevice::ReadOnly)) {
+ delete result;
+ return NULL;
+ }
+ FILE *f = fdopen(result->file.handle(), "r");
+ assert(f);
+ RawFileReadState state(result->points, errors);
+ pt_read_raw(f, 0 /* not compat */, &state, config_cb,
+ time_cb, data_cb, error_cb);
+ result->rec_int = state.rec_int;
+ return result;
+}
+
+
diff --git a/gui/RawFile.h b/gui/RawFile.h
new file mode 100644
index 000000000..7d7fa821e
--- /dev/null
+++ b/gui/RawFile.h
@@ -0,0 +1,58 @@
+/*
+ * $Id: RawFile.h,v 1.3 2006/08/11 19:58:07 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _GC_RawFile_h
+#define _GC_RawFile_h 1
+
+#include
+#include
+#include
+#include
+
+struct RawFilePoint
+{
+ double secs, nm, mph, watts, miles;
+ unsigned cad, hr, interval;
+ RawFilePoint(double secs, double nm, double mph, double watts,
+ double miles, unsigned cad, unsigned hr, unsigned interval) :
+ secs(secs), nm(nm), mph(mph), watts(watts), miles(miles),
+ cad(cad), hr(hr), interval(interval) {}
+};
+
+class RawFile
+{
+ private:
+
+ QFile file;
+
+ RawFile(QString fileName) {
+ file.setFileName(fileName);
+ }
+
+ public:
+
+ QDateTime startTime;
+ int rec_int;
+ QList points;
+ static RawFile *readFile(const QFile &file, QStringList &errors);
+};
+
+#endif // _GC_RawFile_h
+
diff --git a/gui/RideItem.cpp b/gui/RideItem.cpp
new file mode 100644
index 000000000..530676153
--- /dev/null
+++ b/gui/RideItem.cpp
@@ -0,0 +1,138 @@
+/*
+ * $Id: RideItem.cpp,v 1.3 2006/07/09 15:30:34 srhea Exp $
+ *
+ * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "RideItem.h"
+#include "RawFile.h"
+#include "Settings.h"
+#include
+
+static QString
+time_to_string(double secs) {
+ QString result;
+ unsigned rounded = (unsigned) round(secs);
+ bool needs_colon = false;
+ if (rounded >= 3600) {
+ result += QString("%1").arg(rounded / 3600);
+ rounded %= 3600;
+ needs_colon = true;
+ }
+ if (needs_colon || rounded >= 60) {
+ if (needs_colon)
+ result += ":";
+ result += QString("%1").arg(rounded / 60, 2, 10, QLatin1Char('0'));
+ rounded %= 60;
+ needs_colon = true;
+ }
+ if (needs_colon)
+ result += ":";
+ result += QString("%1").arg(rounded, 2, 10, QLatin1Char('0'));
+ return result;
+}
+
+RideItem::RideItem(QTreeWidgetItem *parent, int type, QString path,
+ QString fileName, const QDateTime &dateTime) :
+ QTreeWidgetItem(parent, type), path(path),
+ fileName(fileName), dateTime(dateTime)
+{
+ setText(0, dateTime.toString("ddd"));
+ setText(1, dateTime.toString("MMM d, yyyy"));
+ setText(2, dateTime.toString("h:mm AP"));
+ setTextAlignment(1, Qt::AlignRight);
+ setTextAlignment(2, Qt::AlignRight);
+}
+
+QString
+RideItem::htmlSummary()
+{
+ if (summary.isEmpty()) {
+ QFile file(path + "/" + fileName);
+ QStringList errors;
+ raw = RawFile::readFile(file, errors);
+ summary = ("