Compare commits

...

450 Commits

Author SHA1 Message Date
Eric Murray
b87aab0302 fix manual ride entry crash when no zones file
This is a partial patch based on code from Eric Murray.  It changes just
enough of ManyalRideDialog to fix a crash that otherwise occurs when the
user doesn't have a zones file.
2009-09-19 09:22:42 -07:00
Mark Liversedge
ecdc2288ff Optional padding data with value 0x8012 discovered 2009-09-12 21:30:17 -04:00
Berend De Schouwer
04bb484f8e SplitRide altitude matches CSV2 2009-09-12 21:30:09 -04:00
Sean Rhea
6eeea4a305 simplify GC_BUILD_DATE and GC_VERSION
Use gcc macros for __TIME__ and __DATE__ to compute build date.  This has the
advantage that we don't have to shell out to find the date, though it has the
disadvantage that it doesn't give us any control over format, nor does it
report the time zone.

By default, set GC_VERSION to "(developer build)".  For release versions,
specify GC_VERSION explicitly in gcconfig.pri like this:

  QMAKE_CXXFLAGS += -DGC_VERSION="'\"1.2.0\"'"

It would be nice to specify the git commit id in developer builds.  On the
other hand, the developer could always have uncommitted changes, so the git
commit id doesn't really make for a completely reproducible build.  It's also
a pain to get ahold of in Windows.
2009-09-11 09:13:20 -04:00
Mark Liversedge
af676f24eb WKO Import no longer aborts bulk import on first file error. 2009-09-11 09:13:20 -04:00
Justin Knotzke
e761091097 main and RideItem now use the global function that returns the correct QSettings. 2009-09-11 08:49:20 -04:00
Justin Knotzke
72c40de966 Added to what Greg has done in regards to GC's settings. Fixed a bug regarding where power.zones file was being saved to. 2009-09-11 08:49:20 -04:00
Greg Lonnon
243a28bb87 the settings code was leaking and it was copy/pasted in a few files.
created a method to find QSettings (settings.h) and stopped it from leaking.

The leak looked like this...

==7800==    at 0x4C2726C: operator new(unsigned long) (vg_replace_malloc.c:230)
==7800==    by 0x64FD232: (within /usr/lib/libQtCore.so.4.5.0)
==7800==    by 0x64FDB62: QSettings::QSettings(QString const&, QString const&, Q
Object*) (in /usr/lib/libQtCore.so.4.5.0)
==7800==    by 0x4738E5: PfPvPlot::setData(RideItem*) (PfPvPlot.cpp:361)
2009-09-11 08:49:20 -04:00
Julian Simioni
5299810a7e tcx ride requiring AltitudeMeters 2009-09-10 21:27:04 -04:00
Julian Simioni
b86921d90c Fixed altitude in .tcx files.
One-liner to correctly parse altitude in Garmin .tcx files.
2009-09-10 21:21:59 -04:00
Mark Liversedge
570b2ffc73 Added support for Cycleops 300PT in WKO import 2009-09-10 21:14:55 -04:00
Justin Knotzke
65ee84f948 Changed the label from mm to m as that's what is displayed. 2009-09-09 07:25:57 -04:00
Berend De Schouwer
0873317ea7 Fix PfPV divide 1000 2009-09-09 07:17:43 -04:00
unknown
236878ff7f Fixes editing of notes
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-09-08 18:25:50 -04:00
unknown
f71d329f14 Fixes major bug putting AppData in wrong place on Vista & Win7
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-09-08 18:25:42 -04:00
Robert Carlsen
eb0a40ba8e Update version number to 1.2.0 for imminent release. 2009-09-07 19:45:48 -04:00
Greg Lonnon
e7c7a43b8d fixed the following use of a uninitialized variable
==30062== Conditional jump or move depends on uninitialised value(s)
==30062==    at 0x49070E: ElevationGain::perPoint(RideFilePoint const*, double, RideFile const*, Zones const*, int) (BasicRideMetrics.cpp:114)
==30062==    by 0x491592: PointwiseRideMetric::compute(RideFile const*, Zones const*, int, QHash<QString, RideMetric*> const&) (RideMetric.h:54)
2009-09-07 12:27:08 -04:00
Rhea@.(none)
29ad88de64 rearrange src.pro for Windows 2009-09-06 19:30:03 -04:00
Berend De Schouwer
879b1f6a2e Use CSV filename as timestamp 2009-09-06 18:35:49 -04:00
Sean Rhea
50428b5586 try three times to get PT version string
Sometimes we miss the 'V' in 'VER' on the first one, but a subsequent one
lines up right.  Patch from Dan Connelly.
2009-09-06 18:25:43 -04:00
Sean Rhea
dc8877eb6a turn off tooltips in PowerHist
With some versions of Qt/Qwt, tooltips cause an infinite recursion.  We don't
know why this happens yet, but this patch at least prevents crashes while we
figure it out.
2009-09-06 14:13:31 -04:00
Sean Rhea
fbc3c939e2 remove unused functions 2009-09-06 14:08:19 -04:00
Justin Knotzke
d65fd2a4d0 This should never have made it to github
Revert "Revert "First line of Notes in WKO imported file now has sport followed by workout code. Improves Calendar display.""

This reverts commit 3567012046.
2009-09-05 22:32:33 -04:00
Justin Knotzke
656f548896 This should never have made it to github.
Revert "Revert "guard against all negative values in PowerHist""

This reverts commit d785ca5a0f.
2009-09-05 22:31:47 -04:00
Justin Knotzke
c798420cee Test files for the qollector. 2009-09-05 22:27:32 -04:00
Justin Knotzke
d785ca5a0f Revert "guard against all negative values in PowerHist"
This reverts commit df33fe2301.
2009-09-05 22:24:01 -04:00
Justin Knotzke
3567012046 Revert "First line of Notes in WKO imported file now has sport followed by workout code. Improves Calendar display."
This reverts commit 325140af26.
2009-09-05 22:24:01 -04:00
Mark Rages
e7f7decf10 Add support for Quarq ANT+ log import.
The Quarq ANT+ log contains a hex dump of raw ANT+ messages.  This
importer uses the closed-source program "qollector_interpret" to convert
the ANT+ log file into an XML format, then parses that into a RideFile.

qollector_interpret binaries for several platforms may be downloaded from
http://opensource.quarq.us/qollector_interpret

If the qollector_interpret program is not available, the menu selection
for importing Quarq ANT+ will not appear, nor will .qla files be
imported.
2009-09-05 21:28:12 -04:00
Sean Rhea
df33fe2301 guard against all negative values in PowerHist
This patch unifies the way in which we handle negative values in the Power
Histogram by ignoring all values (speed, hr, cadence, power, and torque)
less than zero.

I'm not sure if this is the right way to handle such values long term, but
it sure beats dumping core.
2009-09-04 19:52:33 -04:00
Mark Liversedge
325140af26 First line of Notes in WKO imported file now has sport followed by workout code. Improves Calendar display.
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-09-04 10:52:10 -04:00
Mark Liversedge
afb3fb6d62 WKO Fix for 64bit, Big Endian and Interval should be 0
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-09-04 10:00:03 -04:00
Sean Rhea
7bd0d0326c fix include <> --> "" 2009-09-02 21:25:58 -04:00
Rhea@.(none)
a41226ade5 fix newlines 2009-09-02 21:17:35 -04:00
Berend De Schouwer
bab4063fd0 Resize for small screens 2009-09-02 12:46:17 -04:00
unknown
f4336ec87c Fixes stack overflow crash bug 2009-09-02 10:54:13 -04:00
Sean Rhea
9fb219019e move 'CONFIG += static debug' into gcconfig.pri 2009-09-02 10:45:13 -04:00
Thomas Weichmann
32e30f7ccd Changed title of Power Histogram Tab to Histogram Analysis 2009-09-02 10:38:28 -04:00
Sean Rhea
75e2c43cdd don't compile/link D2XX.cpp if isEmpty( D2XX_INCLUDE )
As suggested by Berend De Schouwer.
2009-09-02 10:36:02 -04:00
Claus Assmann
0c08ff093d Need unistd.h for unlink(2). 2009-09-02 10:30:33 -04:00
Justin Knotzke
faf01deb6f Took out a RideCalendar::addRide() Adding a ride debug statement. 2009-09-01 06:38:07 -04:00
Sean Rhea
dc50cf79e6 PowerHist cleanup: trust QVector::resize()
QVector::resize() already does exponential capacity growth and zero fills new
elements, so there's no reason for us to be doing either by hand.  This change
simplifies our code substantially.
2009-09-01 06:19:54 -04:00
Berend De Schouwer
4399d1ad81 RidePlot/ErgomoCSV/ErgomoHU Interval consistency 2009-09-01 06:12:34 -04:00
Berend De Schouwer
86cfac2d9e Calendar add-remove rides 2009-09-01 06:12:34 -04:00
Sean Rhea
a1bbf4d50f bug fix: no altitude in srm files
This bug was introduced in 5c0bdd89, which added an alt member to
SrmDataPoint without initializing it.
2009-09-01 05:38:18 -04:00
Claus Assmann
debd870811 There are more than just 3 OSs, make "Unix" the default. 2009-09-01 05:29:36 -04:00
Robert Carlsen
ad0fdd243e Changed declaration of QMAKE_CXXFLAGS to include possible settings from gcconfig.pri. ( = --> += ) 2009-08-31 11:16:43 -04:00
Sean Rhea
d03defb2da ignore empty include paths 2009-08-31 05:36:17 -07:00
Sean Rhea
a5dd700033 add D2XX_INCLUDE path
Also remove Windows-specific include path in D2XX.h, as setting
D2XX_INCLUDE in gcconfig.pri will accomplish the same thing.
2009-08-30 15:08:48 -07:00
Berend De Schouwer
ec8d3e9949 Add Calendar 2009-08-30 14:49:25 -07:00
Berend De Schouwer
55f0b19ff5 Add altitude for Ergomo CSV 2009-08-30 09:35:10 -07:00
Sean Rhea
75a5d66a6a test csv file with -1 hr values
This cvs file from Phil Skiba contains -1 values for hr that were
crashing the power histogram plot.

This is also the first entry into the test/rides directory, in which I
hope to store all ride files that have ever crashed any version of GC.
2009-08-30 09:25:08 -07:00
Sean Rhea
0200f3189c handle non-standard include/lib paths 2009-08-30 09:03:16 -07:00
Robert Carlsen
acaa6e1f1a Restoring src.pro to use Mac Carbon framework 2009-08-29 18:34:18 -04:00
Mark Liversedge
b37c80f849 Fix .notes created in ~/Library/GoldenCheetah rather than ~/Library/GoldenCheetah/<cyclist>/
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-08-29 18:30:27 -04:00
Mark Liversedge
1d85f94f7b Stop .notes file being created in / when browsing through WKO files.
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-08-28 18:40:01 -04:00
Mark Liversedge
ce2ae9a8d3 Rudimentary calculation of Distance from Time/Speed when Distance is not available as a graph
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-08-27 10:54:56 -04:00
Mark Liversedge
a7023c2ea5 Added Fix for Garmin 205/305
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-08-26 14:23:16 -04:00
Robert Carlsen
a970b12f68 Fix for PowerHist crash with HR of -1 2009-08-26 10:47:50 -04:00
Mark Liversedge
d66ca54b41 Updated WKO import to support Altitude in RideFile->appendPoint()
Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-08-25 13:35:21 -04:00
Mark Liversedge
e9e3262caa Adjustments to wko specific source files 2009-08-25 09:59:23 -04:00
Mark Liversedge
d2efc75948 Added WKO RideNotes, applied style guide, fixed CP calc, check version of file 2009-08-25 09:59:22 -04:00
Mark Liversedge
e281cfb444 Src.pro and MainWindow.h changes for WKO import 2009-08-25 09:59:22 -04:00
Mark Liversedge
3722c3bdf0 Changed MainWindow.cpp to attach WKO file import 2009-08-25 09:59:22 -04:00
Mark Liversedge
59805db47c Initial support for WKO file import 2009-08-25 09:59:22 -04:00
Robert Carlsen
143355459d Fix for swapped cadence and altitude data 2009-08-25 09:22:52 -04:00
Thomas Weichmann
5c0bdd8969 Changes to add altitude data to allplot & elevation gained to ride metrics 2009-08-25 06:18:20 -04:00
Sean Rhea
2be608410a fix inexplicable naming conflict
Somehow this SrmData conflicts with the one in srm.h.  I would have expected
the compiler to flag that, but it didn't.  Weird.
2009-08-24 21:21:44 -07:00
Sean Rhea
bf41ffdb10 use gcconfig.pri for local config 2009-08-24 20:08:55 -07:00
Sean Rhea
0c82211bf6 restructure srm d/l code to improve clarity
Use class wrappers around the srmio library to insure that we always
close every opened device and free all allocated data.  Includes other
little bits of stylistic cleanup.
2009-08-24 20:03:07 -07:00
Sean Rhea
20b7c401ad naked new okay for Qt classes with parent pointers 2009-08-24 19:54:32 -07:00
Sean Rhea
59d0b0ede1 coding style guidelines 2009-08-24 19:42:12 -07:00
unknown
086eb6c26b Fixes Win32 Build again 2009-08-22 12:13:06 -04:00
Justin Knotzke
d3ea473f60 This adds timezone support for TCX files. - Julian Simioni 2009-08-21 20:26:47 -04:00
Justin Knotzke
5a453fb210 Misc fixes by Julian Simioni.. Thanks. 2009-08-21 20:22:59 -04:00
Justin Knotzke
3f7938f344 Julian Simioni's patch that uses standard OS directories for storing settings and libraries as well as settings.
USB Stick support still supported.
2009-08-21 19:34:19 -04:00
Sean Rhea
e5ec325caf don't clear/close device if open fails 2009-08-18 04:51:48 -07:00
Sean Rhea
278fd14af7 add option to clear srm memory after download 2009-08-18 04:37:50 -07:00
Sean Rhea
453a398663 fix include path to srmio.h 2009-08-18 03:12:19 -07:00
Sean Rhea
1adbef36e8 add include path for srmio 2009-08-15 17:55:11 -07:00
Sean Rhea
1409d9ec34 use local install of libsrmio.a
Rather than committing the srmio code to the GC repository, let the user
download and build it on their own.  It can be found at

  http://www.zuto.de/project/file/srmio/

or

  git://github.com/rclasen/srmio.git
2009-08-15 17:28:31 -07:00
Sean Rhea
e31db9f837 direct srm d/l -- ghetto, but working
The ghetto part is that we just read the device path out of the device name,
then pass that path directly to Rainer's srmpc_get_data, rather than passing
it an abstraction of a serial port.  As such, this code will only work on
Unix-like operating systems.  But it does work, and that's a good start.
2009-08-15 14:15:46 -07:00
Sean Rhea
d6022ec28c add Prolific 2303 to serial device paths 2009-08-15 14:15:45 -07:00
Sean Rhea
31c3b508bb fix segfault on very short rides 2009-08-15 14:15:45 -07:00
Sean Rhea
9ecf06a334 add qmake file to build static lib 2009-08-15 14:15:45 -07:00
Sean Rhea
83fb5c6968 srmio version 0.0.3 2009-08-15 14:15:45 -07:00
Justin Knotzke
b5f1e64cae Fixed a bug where MPH was always shown in the AllPlot even when Metric was selected. 2009-08-15 07:26:38 -04:00
Eric Murray
e3e1f8fe82 Manual ride entry updates
fix to use last N days worth of rides for BiksScore estimates
fix for skipping some rides in BikeScore estimates
skips rides with zero Bikescore for BikeScore estimates
hitting enter on ManualRide entry dialog doesn't write file
better checking for inputs on ManualRide dialog

Signed-off-by: Robert Carlsen <robert@robertcarlsen.net>
2009-08-12 21:04:36 -04:00
Robert Carlsen
e8a7a4bf4d Fixed constructor bug when using a NULL value and qwt5. ie. unit(NULL) -> unit(0) 2009-08-11 17:15:54 -04:00
Justin F. Knotzke
622516b63d This code should now allow GC to be run off a USB stick.. or the Qollector. If GC finds a Library/GoldenCheetah next to the executable, it will use that location to store all of its settings and Libraries. Otherwise, it reverts to how GC handled settings previously. 2009-08-11 06:07:36 -04:00
Sean Rhea
41063d069d avoid static constructor ordering bug 2009-08-10 21:45:46 -07:00
Sean Rhea
a237779dc8 QString.h --> QString 2009-08-09 22:04:59 -07:00
Sean Rhea
fc4108904f treat hsecsincemidn as a signed integer 2009-08-09 22:04:51 -07:00
Eric Murray
6ce20ccb29 Fixed calculation errors in manual ride BikeScore estimates 2009-08-09 15:43:07 -07:00
Sean Rhea
83ce4d1f59 abstract instructions; use combo for port, too 2009-08-09 15:34:53 -07:00
Sean Rhea
5461e07fcd add combobox to select device type 2009-08-09 15:34:46 -07:00
Sean Rhea
502cb4b60f abstract Device to support multiple device types 2009-08-09 15:34:39 -07:00
Sean Rhea
9624cd03a9 move all PT-specific code out of DownloadRideDialog 2009-08-09 15:34:32 -07:00
Sean Rhea
a01bc21ece localize PT-specific code in DownloadRideDialog 2009-08-09 15:33:49 -07:00
Sean Rhea
f588082449 clean up headers 2009-08-09 15:33:41 -07:00
Sean Rhea
356ee341b2 move calc of d/l status str to PowerTapDevice
...thereby making DownloadRideDialog a little more device-agnostic.
2009-08-09 15:33:35 -07:00
Sean Rhea
563285ab9d move filename calculation to downloadClicked()
...in preparation for moving more of DownloadRideDialog into PowerTapDevice.
2009-08-09 15:33:23 -07:00
Sean Rhea
6dee00e5b8 move PT download code into PowerTapDevice.(h|cpp) 2009-08-09 15:33:16 -07:00
Sean Rhea
6e56b8652f rename PowerTap.(h|cpp) to PowerTapUtil.(h|cpp)
...in preparation for separating out a PowerTapDevice class.
2009-08-09 15:33:02 -07:00
Sean Rhea
b4553ed189 ManualRideDialog doesn't need PowerTap functions 2009-08-09 15:32:54 -07:00
Sean Rhea
1c732ed2cc rename Device to CommPort
I hate to change so many lines of code just for a little rename, but I want to
distinguish between "devices", like the PowerTap and SRM, and "communications
ports", like the serial port and the native D2XX drivers.  This work is in
preparation for adding direct download support for the SRM.
2009-08-09 10:01:00 -07:00
Sean Rhea
f765cef661 fix unused parameter warning 2009-08-09 09:29:11 -07:00
Sean Rhea
e9bae94b83 fix unused variable warnings 2009-08-08 10:39:22 -07:00
Sean Rhea
913b5b6c73 add support for .srm version 7 2009-08-08 10:39:12 -07:00
Justin Knotzke
eb06e3e6d7 Manual patch entry by Eric Murray. Users can now enter in a manual entry
based on distance or time.
2009-08-07 21:31:06 -04:00
Justin Knotzke
ca4278f904 Manual patch entry by Eric Murray. Users can now enter in a manual entry based on distance or time. 2009-08-07 21:25:22 -04:00
Sean Rhea
df9519a027 git replaces svn 2009-08-01 19:33:26 -07:00
Sean Rhea
d79a6f5885 reminder to update web page about git 2009-08-01 14:51:30 -07:00
Sean Rhea
71fcf7ba5d Merge branch 'master' of git@github.com:srhea/GoldenCheetah 2009-08-01 14:23:00 -07:00
Sean Rhea
ad8e5015e1 fix zero-torque bug in PowerHist 2009-08-01 14:04:48 -07:00
Justin Knotzke
d500064da2 Fixed a small bug whereby we weren't deteting Linux correctly (or at all for that matter)
It now uses the standard QMAKE -spec for #ifdefs
2009-08-01 16:52:49 -04:00
Justin Knotzke
9f65afff15 Try to deduce Ergomo filenames when importing into GC as CSV.
Berend De Schouwer

Instead of importing with the creation time -- which is when the file
was downloaded -- attempt to get a more useful time.
2009-07-01 09:28:18 -04:00
Justin Knotzke
07b8d15769 Ergomo CSV files list both a TIME field at column 0, and a PAUSE field
at column 10.

The TIME (L_SEC) field appears to always increment one recording
interval at a time -- however it always increment consistently.  When
there is a pause, the TIME field increments as if there was NO pause.

For example:

   11,   0.015,    0,   0, 12.7,   0,1350.2, 11.7,  0,     0
   12,   0.015,    0,   0, 12.7,   0,1350.0, 11.7,  0,     0
   13,   0.015,    0,   0, 12.7,   0,1350.0, 11.7,  0, 35607
   14,   0.015,    0,   0,  2.9,  70,1095.2,  4.7,  0,     0
   15,   0.021,    0,   0,  2.9,  70,1094.0,  4.7,  0,     0

At the 13th second, there was a pause for 35607 seconds.  The 14th
second is actually the 14+35607 second.

The attached patch loads those files correctly.  This allows the "Ride
-> Split Ride..." menu option to work as expected.

Thanks Berend De Schouwer
2009-07-01 09:20:34 -04:00
Robert Carlsen
f8d6190553 PfPv patch for missing cadence data from Dan Connelly 2009-06-30 00:33:10 -04:00
Robert Carlsen
ee7a953e53 added .gitignore for doc dir 2009-06-26 18:44:41 -04:00
Robert Carlsen
0754c43b1f updating FTDI instructions on user guide page 2009-06-26 18:12:40 -04:00
Robert Carlsen
64751d7585 Adding missing assert.h include 2009-06-24 14:43:56 -04:00
Robert Carlsen
263e28b6b6 Restoring ifdef for D2XX on win32 2009-06-23 13:18:00 -04:00
Robert Carlsen
8dc03b0cb3 Merge branch 'clean' into hub 2009-06-23 12:48:44 -04:00
Sean Rhea
7f1a19930c update title again, rewrite introduction text
Say more about what GC actually does, rather than talking about future goals.
2009-06-23 09:23:30 -07:00
Robert Carlsen
8af2bb02e5 Adding .gitignore files. Feel free to tweak them for your environment 2009-06-23 12:20:56 -04:00
Robert Carlsen
0b9458d954 Bug fixes for PfPv and TCXParser from Ned 2009-06-23 12:19:56 -04:00
Sean Rhea
2ddd87faae web page edit: more inclusive title 2009-06-23 08:48:54 -07:00
Robert Carlsen
13ec408aa3 Adding in PolarRideFile.cpp/h 2009-06-23 09:20:00 -04:00
Justin Knotzke
47b4cf1c44 Damien's patch to support CS600 Polar files. 2009-06-23 06:35:53 -04:00
Robert Carlsen
e973e51281 updating the website docs 2009-06-23 02:06:34 -04:00
Justin F. Knotzke
38f8283ece Threw in an ifdef to fix differences between qwt 5.2 and qwt 5.1.2 2009-06-22 12:02:21 +00:00
Robert Carlsen
e8f2877539 Adjusting weekly plot axis label font size slightly for Mac. Small fonts (< 8pt) are aliased by default in OS X. 2009-06-22 05:37:13 +00:00
Robert Carlsen
52821ee647 Small bug fix for lo and hi margins values. 2009-06-22 05:29:50 +00:00
Justin F. Knotzke
fcdd894c52 Small fixes post Dan's Mega Patch.. 2009-06-22 03:50:04 +00:00
Justin F. Knotzke
d9baad3545 Missing settings.h 2009-06-22 03:23:37 +00:00
Justin F. Knotzke
8d3fbf85e1 Berend De Schouwer
A little tooltip highlighter to let you know power what you are looking at.
2009-06-22 03:18:05 +00:00
Justin F. Knotzke
af633953ec Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 03:10:46 +00:00
Justin F. Knotzke
76f6cf3f7f Added DaysScaleDraw.h for Dan's mega patch 2009-06-22 03:10:09 +00:00
Justin F. Knotzke
3ab2c91ce2 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:27:11 +00:00
Justin F. Knotzke
13deea6f31 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:26:35 +00:00
Justin F. Knotzke
3723185439 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:25:09 +00:00
Justin F. Knotzke
69775fccb9 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:24:28 +00:00
Justin F. Knotzke
08fc58454f Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:24:21 +00:00
Justin F. Knotzke
6caf431d05 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:24:16 +00:00
Justin F. Knotzke
f11d7e1bd9 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:24:10 +00:00
Justin F. Knotzke
fb4514b5fe Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:24:05 +00:00
Justin F. Knotzke
e8cddb104f Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:37 +00:00
Justin F. Knotzke
1652491be1 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:30 +00:00
Justin F. Knotzke
12f91a9bf1 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:23 +00:00
Justin F. Knotzke
3b4e902870 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:17 +00:00
Justin F. Knotzke
d2545348a1 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:11 +00:00
Justin F. Knotzke
b90598eb39 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:06 +00:00
Justin F. Knotzke
6ccaac6123 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:23:00 +00:00
Justin F. Knotzke
15a546927e Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:56 +00:00
Justin F. Knotzke
bf534977b8 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:51 +00:00
Justin F. Knotzke
d92468a625 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:45 +00:00
Justin F. Knotzke
47044708d9 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:39 +00:00
Justin F. Knotzke
9fefe27a38 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:35 +00:00
Justin F. Knotzke
483589c372 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:30 +00:00
Justin F. Knotzke
420b2b6b44 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:25 +00:00
Justin F. Knotzke
84f4250e4a Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:18 +00:00
Justin F. Knotzke
2e1801d8bd Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:22:13 +00:00
Justin F. Knotzke
4453d690b3 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:21:16 +00:00
Justin F. Knotzke
b2acd45c8d Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:21:12 +00:00
Justin F. Knotzke
ea58d961a6 Dan Connelly's MEGA patch.
It includes both powerzones and weekly summary plots.

  Thanks Dan.
2009-06-22 02:21:10 +00:00
Justin F. Knotzke
40120a373e Crank length updated live (you must change the ride) to see the change. Before this patch you had to restart GC. Thanks to Berend. 2009-06-21 16:54:35 +00:00
Justin F. Knotzke
ca26791dcb Berend De Schouwer
A little tooltip highlighter to let you know power what you are looking at.
2009-06-21 14:10:16 +00:00
Justin F. Knotzke
ea49b24337 Berend De SchouwerI made it possible to set the default cranklength.
I made it a combo box to avoid having to take care of invalid inputs.
2009-06-21 14:08:05 +00:00
Justin F. Knotzke
02dce7245b Berend De SchouwerI made it possible to set the default cranklength.
I made it a combo box to avoid having to take care of invalid inputs.
2009-06-21 14:04:37 +00:00
Robert Carlsen
12db84372f Fixing a case-sensitivity bug with csv files. 2009-06-17 13:56:53 +00:00
Justin F. Knotzke
78766b0752 Fix by Dan Connelly for a weekly summary bug that showed different summaries for the same week when moving the picker. 2009-06-02 00:09:28 +00:00
Justin F. Knotzke
5c2f829519 Danniel Connelly's patch regarding Histogram and the implementation of a Y Axis. 2009-05-16 20:57:39 +00:00
Sean C. Rhea
015862460e cleanup related to parseRideFileName
It was lame that I had to add the "xml" suffix to this regex.  It should be
enough to add a RideFile subclass.  This patch also does a more robust job of
setting the notesFileName.
2009-05-03 15:56:00 +00:00
Sean C. Rhea
4633562138 oops! 2009-05-03 15:38:15 +00:00
Sean C. Rhea
aeec208f10 read xml files
The current RideFile type associates a unique interval number with each sample
point, meaning that intervals can't overlap.  It also names intervals as
integers, not strings.  So for now, XmlRideFile just orders intervals by their
order in the xml file and names them by their order in this list (starting
with zero, to match convention).  It then associates each sample with the
lowest-named interval into which the sample falls.  This strategy means that a
raw file exported to xml will have the same interval locations and names when
read back in as xml.
2009-05-03 01:41:12 +00:00
Sean C. Rhea
37fdc5eefd document encoding string can't contain spaces 2009-05-03 01:22:33 +00:00
Sean C. Rhea
095ad55763 handle failure to read ride file gracefully 2009-05-03 01:08:38 +00:00
Sean C. Rhea
9dc3c00023 export to XML functionality 2009-05-02 22:58:57 +00:00
Justin F. Knotzke
7e6ee45d9a Dan found a bug in how the dates were written to file. 2009-04-17 12:03:25 +00:00
Robert Carlsen
8506306627 Updating the Delete Ride dialog with caution icon and "Cancel"/"Delete" buttons. Also wrapped strings in Qt translate functions. 2009-04-14 18:04:37 +00:00
Robert Carlsen
604c24147f Delete ride function from Ned Harding. 2009-04-14 15:48:13 +00:00
Robert Carlsen
4604f62e23 Updating the minor version to 1. 2009-04-10 20:13:59 +00:00
Justin F. Knotzke
0f0e817ff0 Ned's install script for Win32. 2009-04-10 14:33:13 +00:00
Justin F. Knotzke
bc17462c41 Fix by Daniel Connelly to ensure Zones Ranges are more continuous. 2009-04-10 14:06:18 +00:00
Robert Carlsen
6222fe888a Committing Ned Harding's win32 building updates 2009-04-08 19:22:11 +00:00
Robert Carlsen
c9a641ea61 Committing Ned Harding's patches for building on win32. 2009-04-08 07:12:24 +00:00
Robert Carlsen
93b5007746 From Ned Harding: Basic implementation of dflcn header for win32 2009-04-08 06:59:49 +00:00
Robert Carlsen
53f4e6f224 From Ned Harding: Basic implementation of dflcn header for win32 2009-04-08 06:58:59 +00:00
Justin F. Knotzke
48bd6b2fec Fixed skiba_xpower bug. 2009-04-07 23:29:18 +00:00
Justin F. Knotzke
4861d91c95 Small fix to LTM. Still in comments 2009-04-07 22:28:14 +00:00
Justin F. Knotzke
4ac1444f5c Small fix to LTM. Still in comments 2009-04-07 22:23:24 +00:00
Justin F. Knotzke
3fe277a9a5 Small fix to LTM. Still in comments 2009-04-07 22:22:39 +00:00
Justin F. Knotzke
0d1066a020 Quick LTM fix. This is all still in comments. 2009-04-07 22:15:36 +00:00
Justin F. Knotzke
22be6e216d Stupid svn and case sensitivity 2009-04-07 22:14:57 +00:00
Justin F. Knotzke
7209df0764 Small fix to LTM. Still in comments 2009-04-07 22:09:52 +00:00
Justin F. Knotzke
3f1d3f2adb Small fix to LTM. Still in comments 2009-04-07 22:09:22 +00:00
Justin F. Knotzke
73814a4df8 Small fix to LTM. Still in comments 2009-04-07 21:32:27 +00:00
Robert Carlsen
9228211e15 restoring static linking for qwt lib and removing reference to ftdi lib (handled in D2XX.h/cpp at runtime) 2009-04-06 16:20:06 +00:00
Justin F. Knotzke
0f218f7cdd Wrong file.. 2009-04-06 10:13:59 +00:00
Justin F. Knotzke
4bc62f7a6e Wrong file.. 2009-04-06 10:13:01 +00:00
Justin F. Knotzke
4978209289 This is the new Metrics SQL Database support. 2009-04-06 01:24:48 +00:00
Justin F. Knotzke
57e5fea35b This is the new Metrics SQL Database support. 2009-04-06 01:24:30 +00:00
Justin F. Knotzke
b7c40388f2 SplitRideDialog. Missing from r300 Checkin. 2009-04-06 01:05:40 +00:00
Justin F. Knotzke
c7652caeec Accidentally took out ppc from the mac build. 2009-04-06 01:03:05 +00:00
Justin F. Knotzke
21d5576393 Split Ride.
It offers to split at any time gap over 30 seconds and also at any interval.  
If the time gap is over 5 minutes it defaults to checked, otherwise it 
defaults to unchecked.

Anywhere you check, it will split the ride at that point overwriting the or
original ride with a shorter one and creating new rides after the split points
 The original would get renamed with a .bak so it could be recovered.
2009-04-06 01:01:17 +00:00
Justin F. Knotzke
3db61084a1 Fixed a possible memory bug. Thank you to Dan Connelly 2009-04-03 00:33:06 +00:00
Justin F. Knotzke
56cc8b084d Tom Montgomery's patch:
I have made changes to the Import CSV dialog box; the new (proposed)  
behaviour is as follows:

  At first, the datePicker widget and OK button are disabled.
  The datePicker is preset to today's date (no longer really  
necessary, but the code is there).
  User clicks 'choose a file' and the usual file browser appears.
  If a file is selected, its creation date is stuffed into the  
datePicker.
  On return from the browser, the datePicker and OK buttons are re- 
enabled.
  User can modify the ride date, in case the file upload was not done  
on ride day.
  User clicks OK, the file is imported as before.
2009-03-27 18:02:34 +00:00
Justin F. Knotzke
6cfccfc92f This patch by Tom Montgomer now uses the current date when importing a CSV file instead of defaulting to 2000.
Thanks Tom.
2009-03-25 23:21:47 +00:00
Justin F. Knotzke
f328248582 GC now support the latest v10 iBike CSV import.
Thanks to Tom Montgomery
2009-03-25 23:15:18 +00:00
Justin F. Knotzke
9d22cc3dc8 Thanks to Ned Harding, Golden Cheetah now support Ant+ Sport. Thanks Ned. Much appreciated.
J
2009-03-18 17:59:46 +00:00
Justin F. Knotzke
a65460d5d8 2009-02-22 15:35:43 +00:00
Justin F. Knotzke
ee3b9f46b9 Took out DBAccess references. 2009-02-22 15:28:26 +00:00
Justin F. Knotzke
d769625f5c Took out DBAccess references. 2009-02-22 15:26:15 +00:00
Justin F. Knotzke
071c9b1071 Took out MetricView references. 2009-02-22 00:49:00 +00:00
Justin F. Knotzke
b6a902ebd9 The critical power plot show maximum average power for all rides.
But you don't know when you reach this value.

I propose this small patch to show the value and the date like on this print screen.

Damien
2009-02-22 00:08:34 +00:00
Justin F. Knotzke
9e73576fba The critical power plot show maximum average power for all rides.
But you don't know when you reach this value.

I propose this small patch to show the value and the date like on this print screen.

Damien
2009-02-22 00:07:19 +00:00
Justin F. Knotzke
b046ae538b The critical power plot show maximum average power for all rides.
But you don't know when you reach this value.

I propose this small patch to show the value and the date like on this print screen.

Damien
2009-02-22 00:07:05 +00:00
Justin F. Knotzke
52b2049949 Damian Grauser's patch which throws a toggle in the Ride Plot graphs which
will toggle between distance and time.

Thanks Damien.
2009-02-14 23:52:10 +00:00
Robert Carlsen
92749ac705 Identify the device type as "Garmin TCX" when reading from a tcx file. 2009-01-26 18:58:25 +00:00
Sean C. Rhea
9e4d237ce9 pop up a warning if we can't find libftd2xx 2009-01-24 20:09:13 +00:00
Sean C. Rhea
a6f269363e load libftd2xx via dlopen rather than linking to it with ld,
so that GC won't crash if it's not there
2009-01-24 17:50:06 +00:00
Robert Carlsen
03e2f95c43 Changed the AvgCadence unit from "bpm" to "rpm". 2009-01-24 00:22:59 +00:00
Justin F. Knotzke
f7ea9b236e This changes the power histogram implementation to use the QVector
data type instead of dynamically allocating and freeing arrays. No
memory leak here, but it's an low hanging fruit type of example of
what kind of changes we can do to reduce the amount of explicit 
dynamic memory management.

   --jtc
2009-01-17 19:36:31 +00:00
Justin F. Knotzke
7026520ec4 This patch changes the weekly summary to include the number of seconds
in total time riding, which previously only included hours and minutes.
A few seconds over a week shouldn't account for much, but I think it's
desirable to use the same resultion for data values across the product
for consistancy if nothing else.

   --jtc
2009-01-17 19:33:49 +00:00
Robert Carlsen
842303029c Updated the Pf/Pv plot curve to use antialiased ellipses rather than points. 2009-01-14 05:21:52 +00:00
Robert Carlsen
1292a5f8e9 Applied JTC's P10, changing DatePickerDialog and RideFile objects to use boost::scoped_ptr to ensure that the objects are deleted. He recommends that we extend this technique to replace other raw pointer / delete methods.
Also, changed PfPvPlot so it will look for the CP from the power.zones file and use that rather than a hard coded value.
2009-01-12 17:15:54 +00:00
Robert Carlsen
a8dad052fd added version 1.0.277 release 2009-01-10 01:41:31 +00:00
Robert Carlsen
50a9de052c from jt conklin: fixed a possible memory leak in the weekly summary. 2009-01-09 23:43:52 +00:00
Robert Carlsen
5a00528f4d Changed the update signal from returnPressed() to editingFinished() for the lineEdit fields in the ride plot and power histrogram views. 2009-01-08 06:47:33 +00:00
Robert Carlsen
b4584baf03 Adding iBike CSV import support. 2009-01-07 01:28:09 +00:00
Robert Carlsen
d849834070 Added escaping slashes to the build date. It may only work on linux/max os x, I haven't had a chance to try it on win32. 2009-01-06 02:51:15 +00:00
Robert Carlsen
21a72bc45e Added escaping slashes to the svn version number compiler directive to workaround a qmake issue with using a string value for a define. The modified statement is: \\\"svnversion . | cut -f '2' -d ':'\\\"
It may only work on linux/max os x, I haven't had a chance to try it on win32.

This was noted here: http://www.archivum.info/qt-interest@trolltech.com/2008-09/msg00070.html
2009-01-06 02:26:24 +00:00
Robert Carlsen
264e8b118e Changed from Build Date to Version Numbering system in the about dialog. As discussed on the list, the protocol is major.minor.revision, currently 1.0.271. The major and minor version numbers are set in src.pro; the revision number should be automatically derived using "svnversion . | cut -f '2' -d ':'" in src.pro. 2009-01-06 01:54:01 +00:00
Robert Carlsen
fe5b1300eb Opting to include the svn revision number grab in src.pro 2009-01-06 01:37:32 +00:00
Robert Carlsen
c731525124 Utility to add include the svn revision number to the program version in the about dialog. 2009-01-06 00:32:16 +00:00
Robert Carlsen
dd7c308667 Fix case sensitivity issue in Pages.h for #include <QCheckBox> 2009-01-05 02:30:05 +00:00
Robert Carlsen
700ac5c12d From JT Conklin: Fix to force the aggregateWith() method to use properly use metric units while incrementing when set as a user preference. 2009-01-05 02:23:09 +00:00
Robert Carlsen
f685703ae4 Updating the AddRide method to honor the ride list sorting preference. 2009-01-05 02:03:52 +00:00
Robert Carlsen
8d2edd4c48 Added a user preference to change the Ride List sorting. Default should be ascending by date, as it has been. Disabling the "Sort ride list ascending" preference will sort the ride list descending by date.
Also, added margins to the widgets in the main window.
2009-01-05 01:47:37 +00:00
Robert Carlsen
f8a94dc767 From JT Conklin:
Added support to TCX, CSV and SRM import functions to correctly remember the last import directory.
2009-01-03 20:31:57 +00:00
Justin F. Knotzke
f1ade25fa7 or relatively short (~1 hr) activities, with relatively small (~5w)
bucket sizes, the power histogram often looks short and squat with a
lot of whitespace at the top because the largest bucket may be 3 - 5
minutes, but the y-axis is scaled by adding a constant 10 (minutes) to
the max.

The attached patch scales the y-axis by a factor of 1.1 (The ride plot
does the same scaling, and it appears to work well there).  Now you're
able to see more detail in histogram plots as the curve is not all
squashed along the bottom of the graph.
    -jtc
2009-01-03 18:35:55 +00:00
Robert Carlsen
33ee8daf1e Just standardizing some indention. 2009-01-03 17:25:55 +00:00
Justin F. Knotzke
fb1b79cccf This is a 100% cosmetic issue, but I've never liked the way the
"about" dialog is formatted, with "GoldenCheetah is Licenced under
the GNU General Public Licence." and "Source code can be obtained 
from http://goldencheetah.org/" run together in a single paragraph
because the way the line is broken after "Source".  

The attached patch splits the two sentences into two separate
paragraphs, and centers the entire dialog text.

While this is subjective, I think it looks a lot better.

   --jtc
2009-01-03 11:55:56 +00:00
Robert Carlsen
c11b305239 On the ride plot, the range of speed is typically much smaller than the ranges of power, heart rate, and cadence. As a result, for many activities it's difficult to distinguish much difference.
This update changes the plot to use a right y axis for speed. This makes it easier to see speed differences.

Some other packages use separate y axes for each data type, but as far as I can tell, this is not available with the qwt library used by GC.

   --jtc
2009-01-03 07:13:45 +00:00
Robert Carlsen
baaacda681 Re-enabled the build date QMake flag. Disabled to permit "qmake -spec macx-xcode" to create a valid xcode project file. 2009-01-02 21:26:09 +00:00
Robert Carlsen
1105d60d1f Added compiler flags to build as Mac OS X Universal Binary. May require QWT and Qt to be universal as well. 2009-01-02 21:23:38 +00:00
Justin F. Knotzke
d5997b9fee The Ride Plot currently displays the activity's speed in MPH,
regardless of the Units preference.  This patch checks the preference
and displays it in the appropriate units.

Unlike some of the other cases, I'm checking the value of the Units
setting each time the plot/panel is displayed.  

The patch also pulls the 0.62137119 magic constant into a #define
MILES_PER_KM.  This constant (and it's inverse, KMS_PER_MILE), occurs
in several files, and (IMHO) really deserves to be pulled into a
separate header, but again, that's work for another day.

   --jtc
2009-01-02 20:58:06 +00:00
Justin F. Knotzke
f2187c6965 While browsing the GC code, I found that there was not an explicit
deletion of the Tools (CP Calculator) Dialog, nor was the attribute
set that tells the framework to delete the dialog when it's dismissed.
Since the other dialogs use the attribute, this patch does the same.

   --jtc
2009-01-02 18:09:26 +00:00
Justin F. Knotzke
801a26392e JTC found and fixed a double addWidget error.. 2009-01-02 17:28:59 +00:00
Sean C. Rhea
ec38e8ca1d add device types 2008-05-27 03:53:22 +00:00
Sean C. Rhea
61161a7b5d remember last import path 2008-05-27 03:00:30 +00:00
Sean C. Rhea
5a3c3c8eb7 playing around with export to XML,
may eventually become the "native" GC file format
2008-05-27 02:56:07 +00:00
Sean C. Rhea
a9ce6ae947 add Windows to About dialog 2008-05-23 16:07:20 +00:00
Sean C. Rhea
ef5f2c1a47 from Rob C: change '\' to '/' 2008-05-23 16:04:37 +00:00
Sean C. Rhea
6c3ff75f0b from Rob C: set D2XX to 9600-N-1 to make PT happy on Windows 2008-05-23 16:03:33 +00:00
Sean C. Rhea
1e9c4dffe8 group rides by type, course 2008-05-20 04:57:06 +00:00
Sean C. Rhea
80e113d347 from Justin: "It should fix the bugs with creating a new power.zones file plus
adds two DateEdits to show the start and end of a zone range."
2008-05-19 15:03:26 +00:00
Sean C. Rhea
ff59009f86 Dan Connelly points out that it should be "n * (n + 1) / 2" (plus, not minus). 2008-05-19 14:46:50 +00:00
Sean C. Rhea
9d557b26a1 from Justin: PT reports no data as watts == -1, assume watts == 0 in that case 2008-05-18 15:14:47 +00:00
Sean C. Rhea
91f114f199 from Justin: more hacking on the zones editor 2008-05-18 15:11:21 +00:00
Sean C. Rhea
7c72da0c72 from Justin: icons for windows 2008-05-18 15:01:35 +00:00
Sean C. Rhea
babbaa7e2c Sane failure for rides longer than a week, which usually happen because
someone sets the date on their PT and doesn't reset the device before going
for a ride.  This fix will keep GC working until the user splits the ride
(and until we add a ride-splitting function).
2008-05-18 14:19:37 +00:00
Sean C. Rhea
f91e45d950 fix small memory leak 2008-05-18 14:00:50 +00:00
Sean C. Rhea
6aa6693cad fix awful indentation 2008-05-18 13:58:45 +00:00
Sean C. Rhea
41da1dfc68 smooth updating of progress bar in update_cpi_file 2008-05-14 22:47:54 +00:00
Sean C. Rhea
d91608ecca from Justin: add Serial.(h|cpp) 2008-05-14 14:59:23 +00:00
Sean C. Rhea
25b17de0e4 from Justin: nits 2008-05-14 14:57:00 +00:00
Sean C. Rhea
e6c85a12f4 from Justin: zones editor, switch from FTP to CP 2008-05-14 00:17:10 +00:00
Sean C. Rhea
4eeb656016 update progress dialog while aggregating over .cpi files 2008-05-13 16:42:39 +00:00
Sean C. Rhea
b6f817c4d7 combine cpint.(h|cpp) with CpintPlot.cpp 2008-05-13 16:30:35 +00:00
Sean C. Rhea
83d6989d51 from Justin: more .pro file patches for Windows, plus ToolsDialog 2008-05-13 16:15:22 +00:00
Sean C. Rhea
e63452d521 from Justin: on Windows, hton[ls] is in winsock.h 2008-05-13 03:33:08 +00:00
Sean C. Rhea
812b17c952 don't need 2008-05-13 03:30:49 +00:00
Sean C. Rhea
e2d9a96b52 Justin's changes, heavily editted, to compile on Windows 2008-05-13 02:18:15 +00:00
Sean C. Rhea
133f677b12 renaming to match directory name 2008-05-13 02:16:46 +00:00
Sean C. Rhea
da214db96d move TODO list to the web page as a "wish list" 2008-05-12 16:37:34 +00:00
Sean C. Rhea
98ede8e40c combined lots of emails all into this file 2008-05-12 16:28:58 +00:00
Sean C. Rhea
65c4e1d277 remove old directories 2008-05-12 03:31:25 +00:00
Sean C. Rhea
6e5487ca39 everything in one directory 2008-05-12 03:28:53 +00:00
Sean C. Rhea
b519a0384b insert new rides into allRides in order by date, remove duplicates 2008-05-11 05:18:17 +00:00
Sean C. Rhea
3c998c98b0 Let QTemporaryFile go out of scope so it will really close before calling
rename, since Windows won't rename an open file.  Call remove before rename on
Windows, since Windows rename won't overwrite.  Call setPermissions with
previous value plus all read flags, rather than using write flags explicitly.
Replace lots of asserts with QMessageBox::criticals.
2008-05-10 15:51:26 +00:00
Sean C. Rhea
2f3df889c5 New download code doesn't use Unix-isms. Big thanks to Rob Carlsen for
debugging the weirdness around QTextStream::setFieldWidth.
2008-05-10 03:41:08 +00:00
Sean C. Rhea
60f9724543 remove ctype.h 2008-05-06 05:31:40 +00:00
Sean C. Rhea
7cf5766ab7 remove Unix-specific includes, clean up copyrights somewhat 2008-05-06 05:26:44 +00:00
Sean C. Rhea
91d51a6246 use more QT classes instead of OS-specific stuff 2008-05-06 05:25:31 +00:00
Sean C. Rhea
c832727a03 replace Unix-specific stuff with QT equivalents; convert to C++ 2008-05-05 17:42:58 +00:00
Sean C. Rhea
73fdb7d6ef make listFunctions a pointer so that static initialization order doesn't matter 2008-05-05 02:03:33 +00:00
Sean C. Rhea
bfadc8c043 switch statements suck 2008-05-04 17:32:40 +00:00
Sean C. Rhea
cd080aa48c add JT 2008-05-04 06:56:44 +00:00
Sean C. Rhea
f27ff27c3b TCX importing and Pedal Force vs. Pedal Velocity Chart from
J.T. Conklin (jtc@acorntoolworks.com).
2008-05-04 06:50:34 +00:00
Sean C. Rhea
aed29e150d remove older command-line tools 2008-05-04 05:16:18 +00:00
Sean C. Rhea
4434239235 remove temporary command-line version 2008-05-04 05:13:09 +00:00
Sean C. Rhea
8db3e2c0b2 add power.zones 2008-05-04 05:10:23 +00:00
Sean C. Rhea
f34e0fc74c Lots of code cleanup. All Unix/Mac-specific download code used by graphical
version is now in pt/Serial.cpp, which the .pro files should ignore on win32.
2008-05-04 05:09:32 +00:00
Sean C. Rhea
51165d0acf New download code now working with older VCP driver as well. This code isn't
really fit for human consumption.  I'm just checking it in before I clean it
up in case I go and break something on accident.
2008-05-03 17:52:46 +00:00
Sean C. Rhea
2e21b6e328 now also downloads from serial/usbserial devices and automatically chooses
between them and D2XX based on what's installed
2008-04-12 20:56:12 +00:00
Sean C. Rhea
b47ac76116 set svn:ignore 2008-04-10 17:37:06 +00:00
Sean C. Rhea
17cbe38af8 - Separated download logic from device abstraction layer.
- Now creates a .raw file.
2008-04-10 17:29:30 +00:00
Sean C. Rhea
bce0fbdb95 proof of concept program to test downloading from PT with D2XX drivers 2008-04-02 15:35:32 +00:00
Sean C. Rhea
9f56747a6d added "Find Best Intervals" dialog 2008-03-15 17:30:48 +00:00
Sean C. Rhea
262fa0d9b1 add bit about how to get TextEdit to save a .zones file 2008-03-14 01:52:32 +00:00
Sean C. Rhea
76f161fb46 add BikeScore link 2008-03-14 01:51:50 +00:00
Sean C. Rhea
9e7c9407ff 2008, not 2007 2008-03-11 16:12:19 +00:00
Sean C. Rhea
371bfbdae3 add new images to Makefile 2008-03-11 16:10:15 +00:00
Sean C. Rhea
c70685ee4a added new release 2008-03-11 16:05:58 +00:00
Sean C. Rhea
55356eb221 actually ignore the bad time block that we claim we're going to ignore 2008-03-11 01:02:39 +00:00
Sean C. Rhea
b6691939f1 minor edits 2008-03-10 19:01:35 +00:00
Sean C. Rhea
8ea444f55e fix for QT 4.3.1 and add TM symbol 2008-03-10 18:01:44 +00:00
Sean C. Rhea
7fd58c7f0d first pass at a new User's Guide 2008-03-09 16:49:13 +00:00
Sean C. Rhea
6e60d167b7 added comments 2008-03-08 16:26:12 +00:00
Sean C. Rhea
d5fd8c1234 renamed CombinedFileReader to RideFileFactory 2008-03-08 16:20:43 +00:00
Sean C. Rhea
8b782dff27 Removed RawFile and replaced it with RideFile. I can't remember how we ended
up with both, but they're basically the same class.
2008-03-08 16:11:41 +00:00
Sean C. Rhea
7a43765a25 Now using "Critical Power" instead of "Functional Threshold Power". 2008-03-07 22:36:09 +00:00
Sean C. Rhea
eae6f74087 Use "CP" instead of "FTP" in power.zones to be more compatible with Skiba's
terminology, but still allow "FTP" for backwards compatibility.
2008-03-07 21:37:51 +00:00
Sean C. Rhea
2c2d1eedb6 use minutes, not seconds, as the y-axis says 2008-03-07 20:11:55 +00:00
Sean C. Rhea
26e0336f62 don't try to compute BikeScore if no zones file present 2008-03-07 19:57:33 +00:00
Sean C. Rhea
ad32b3ed94 Adding this here so I don't lose it. 2008-02-21 20:25:04 +00:00
Sean C. Rhea
304fbb49b4 BikeScore, xPower, and Relative Intensity updated to match Skiba's method
more or less exactly.  Also added (TM) to BikeScore in Ride Summary.
2008-02-21 18:41:38 +00:00
Sean C. Rhea
d16330134d Separated out BikeScore as a RideMetric. 2008-02-21 00:51:50 +00:00
Sean C. Rhea
aba9a29a43 All the basic ride metrics now use the RideMetric interface. 2008-02-20 19:24:20 +00:00
Sean C. Rhea
5dc93dccc8 - Added TotalWorkRideMetric.
- Use XML to describe what metrics to display and in what order.
2008-02-20 17:24:45 +00:00
Sean C. Rhea
156a053666 Needed to #include<assert.h>. 2008-02-19 00:31:46 +00:00
Sean C. Rhea
e68caf412b Patch from Rob Carlsen to handle Ergomo CSV imports. 2008-02-17 01:58:53 +00:00
Sean C. Rhea
7db9fd58f4 Separated out total_distance as a ride metric. More to come, especially
ordering and grouping into a display.
2008-02-14 17:57:25 +00:00
Sean C. Rhea
676838bc70 Patch from Justin and Rob.
Adds metric/english unit's dialog and BikeScore calculation.
2008-02-13 17:34:28 +00:00
Sean C. Rhea
76be545eee look for /dev/cu.usbmodem... 2007-11-04 16:38:28 +00:00
Sean C. Rhea
74fef7e468 added link to Rob's Mac Intel build 2007-10-02 21:07:08 +00:00
Sean C. Rhea
cc8a22e2dd moved release images into their own repository 2007-09-25 02:04:18 +00:00
Sean C. Rhea
670e172e26 bug fix release 2007-09-24 03:38:50 +00:00
Sean C. Rhea
f2b2350312 bug fix release 2007-09-24 02:14:32 +00:00
Sean C. Rhea
048ce08ca9 need to qmake now 2007-09-24 02:05:50 +00:00
Sean C. Rhea
f66e94c05a CsvRideFile now only sets its ride startTime if the file is
appropriately named.  cpint now ignores startTime, as it wasn't 
using it anyway.
2007-09-22 16:27:09 +00:00
Sean C. Rhea
d2532341b4 new release 2007-09-18 18:22:28 +00:00
Sean C. Rhea
83e9f67755 call qmake in src before make 2007-09-18 17:24:01 +00:00
Sean C. Rhea
fbff0d6df1 Bug fix for interval markers: must divide by 60.0, not 60,
to get floating point division.
2007-09-18 17:23:39 +00:00
Sean C. Rhea
32dc259642 switched to Justin's code for sample lines so that reading no value
between commas works
2007-09-17 21:57:15 +00:00
Sean C. Rhea
bb3afec9eb ignore Makefile 2007-09-17 21:47:20 +00:00
Sean C. Rhea
f691758e7e converted to qmake 2007-09-17 21:46:37 +00:00
Sean C. Rhea
b03edad5f5 ignore file 2007-09-17 21:43:26 +00:00
Sean C. Rhea
c852547fb3 renamed .pro file 2007-09-17 21:42:59 +00:00
Sean C. Rhea
b97eaa4bf5 converted to C++/qmake 2007-09-17 21:39:30 +00:00
Sean C. Rhea
3c1f6f5e27 converted to C++/qmake 2007-09-17 21:38:06 +00:00
Sean C. Rhea
8b02b6620d removed empty cpint dir 2007-09-17 20:35:59 +00:00
Sean C. Rhea
b723d43bc1 moved cpint code into gui dir 2007-09-17 20:32:19 +00:00
Sean C. Rhea
9e09e4fe70 cpint now using unified ride file framework, too 2007-09-17 18:51:04 +00:00
Sean C. Rhea
42eb95c8b3 unified framework for reading in different ride
file types (raw, srm, and csv)
2007-09-17 18:10:32 +00:00
Sean C. Rhea
134f22f0ca interval markers patch from Justin Knotzke 2007-09-09 16:58:51 +00:00
Sean C. Rhea
4a8ff92437 CSV import code from Justin Knotzke 2007-08-30 16:17:17 +00:00
Sean C. Rhea
b19783c469 Rob Carlsen's patch for ignoring zeros in heart rate during intervals. 2007-08-15 20:46:44 +00:00
Sean C. Rhea
8373bbaf09 Rob Carlsen's patch adding max power to interval summary. 2007-08-15 20:16:13 +00:00
Sean C. Rhea
b8827105b8 some new stuff 2007-08-15 20:11:56 +00:00
Sean C. Rhea
bd73c83b25 new release with cpint fix 2007-08-07 15:22:37 +00:00
Sean C. Rhea
b658247509 note about brltty on Ubuntu 2007-08-07 15:07:10 +00:00
Sean C. Rhea
f402d9afd8 new release with cpint fix 2007-08-07 15:05:59 +00:00
Sean C. Rhea
abbe205dc9 Serious bug fix: while interval duration SHOULD be computed by using
previous data point (i.e., q->secs - p->secs), the duration to multiply
the wattage value by SHOULD NOT be.  Instead, should multiply by
rec_int.  (My ride from Jul 31, 2007 demonstrates the problem.  I
started an interval with a 1-sec wattage of 773 after a ~48 second rest.
Using the old code, that gets credited as 773 watts for 48 secs!)
2007-08-05 14:57:57 +00:00
Sean C. Rhea
253e2083f7 Rob's ride notes patch. 2007-06-10 23:38:15 +00:00
Sean C. Rhea
a9faed9361 oops; need to use QString::arg(), not '+' 2007-06-10 23:04:04 +00:00
Sean C. Rhea
629d9c7db2 Rob's changes 2007-06-10 22:36:46 +00:00
Sean C. Rhea
139c7c03fc add FTP to zones 2007-06-08 22:38:26 +00:00
Sean C. Rhea
ebdbff5f0e new dest dir for install 2007-05-21 04:49:37 +00:00
Sean C. Rhea
fd0e5f76be mention qwt 5.0.1, not older version, as pointed out by Rob Carlsen 2007-05-21 04:47:48 +00:00
Sean C. Rhea
8ebc1a4eb2 Don't output line with zero time (Rob Carlsen says it crashes WKO+), and
print zeroes, rather than nothing, when nm, mph, or watts are zero.
2007-05-21 04:46:26 +00:00
Sean C. Rhea
e2fc9bce5c added zoomer and panner to AllPlot, but needs work still. 2007-05-13 02:28:10 +00:00
Sean C. Rhea
d01184905f fixed bug where interval length was 1 recint too short 2007-05-10 16:30:01 +00:00
Sean C. Rhea
866eec4eee added "About GoldenCheetah" dialog with build date and link to GPL 2007-05-07 22:00:31 +00:00
Sean C. Rhea
27bf8ae1ef fix to work on OS X 2007-05-07 21:09:31 +00:00
Sean C. Rhea
abdf74ca5a use QLineEdit instead of QLabel in CP plot to avoid resizing bug 2007-05-03 23:30:56 +00:00
Sean C. Rhea
0cd28ba8d0 Warning msg said we ignored it when time went backwards in a PT file, but code
was still calling exit(1).  Now it really does ignore it.
2007-04-30 21:39:11 +00:00
Sean C. Rhea
fff8f9a0e6 okay, really fixed this time 2007-04-29 22:40:52 +00:00
Sean C. Rhea
edd39b3d3b fixed problem with download ride button not hilighting when more than one
device is available
2007-04-29 17:27:48 +00:00
Sean C. Rhea
3cdc85e411 added screenshot-weekly.png 2007-04-26 21:25:10 +00:00
Sean C. Rhea
1f8ce83475 new ride summary screenshot showing power zones,
added weekly summary screenshot
2007-04-26 21:24:43 +00:00
Sean C. Rhea
ae1c59d738 new release and description of zones file format 2007-04-26 21:15:53 +00:00
Sean C. Rhea
d87c0500ab new release 2007-04-26 20:32:28 +00:00
Sean C. Rhea
9cdda62cfa new release 2007-04-26 20:32:17 +00:00
Sean C. Rhea
753977743e oops: checked in unnecessary gcc-4.0 bit on accident 2007-04-26 20:26:13 +00:00
Sean C. Rhea
43cf7e0126 - MacOS case-insensitive file system confuses our Time.h with
/usr/include/time.h, so renamed the former.
- Bug fixes for when there is no power.zones file
2007-04-26 20:10:21 +00:00
Sean C. Rhea
e103824538 added zones to the weekly summary, consolidated and cleaned up code 2007-04-25 19:30:55 +00:00
Sean C. Rhea
cfdbe3cc30 working on adding time in each power zone to ride summery 2007-04-24 19:38:49 +00:00
Sean C. Rhea
9dd02b69e4 added weekly summary 2007-04-24 17:55:40 +00:00
Sean C. Rhea
29c760cdeb cpint (cmdline and gui) now works with .srm files, too 2007-04-22 06:25:31 +00:00
Sean C. Rhea
de66eed5ca cpint now uses intervals that are a whole number of seconds long, in
preparation for supporting data files from a variety of devices on the same
machine
2007-04-18 23:03:03 +00:00
Sean C. Rhea
c00bc0504d add cpint dir 2007-04-18 23:01:40 +00:00
Sean C. Rhea
db43014375 added GPL to comments 2007-04-18 22:40:56 +00:00
Sean C. Rhea
b5b16a4768 added GPL to comments 2007-04-18 22:40:24 +00:00
Sean C. Rhea
e1371fab8c fix time riding bug for SRM by making use of rec_int_ms in RawFile rather than
tracking last_secs
2007-04-18 21:19:56 +00:00
Sean C. Rhea
b967d276d1 check that we go forward by at least one recint on block transition 2007-04-18 21:18:12 +00:00
Sean C. Rhea
6952d2b82c get rid of unused parameter warnings 2007-04-18 21:09:52 +00:00
Sean C. Rhea
859b1f2842 converted cpint code to C++ in preparation for making it understand SRM files 2007-04-17 21:55:08 +00:00
Sean C. Rhea
acd48e7112 add srm directory, better handling of qmake 2007-04-17 20:52:30 +00:00
Sean C. Rhea
ff3227f520 move TODO to top level 2007-04-17 20:47:58 +00:00
Sean C. Rhea
432820a24e stuff 2007-04-17 20:47:21 +00:00
Sean C. Rhea
eefe0373c8 - display file even if errors, so long as we get some data
- count time riding as time either pedaling or moving, so that time on trainer
  with front wheel sensor still counts
2007-04-17 20:46:48 +00:00
Sean C. Rhea
56970da25c - fix unused variable warnings
- don't add a bogus interval 0 to files without any intervals
2007-04-17 20:45:05 +00:00
Sean C. Rhea
f2fe16ed51 shouldn't have been added; generated by qmake 2007-04-17 20:44:04 +00:00
Sean C. Rhea
efc99a8a26 add debug to config 2007-04-17 20:43:22 +00:00
Sean C. Rhea
2f7d42c46f commented out printfs 2007-04-17 20:25:30 +00:00
Sean C. Rhea
69612c8d59 anti-aliasing was the source of the poor plot performance on Linux 2007-04-17 19:12:20 +00:00
Sean C. Rhea
7e0731c7f8 allow for multiple imports at once 2007-04-17 18:06:22 +00:00
Sean C. Rhea
abf2f5b349 updates to work with qwt 5.0.1 2007-04-17 18:00:37 +00:00
Sean C. Rhea
7b52bc6f73 csv output uses .csv extension instead of .dat 2007-04-16 21:16:11 +00:00
Sean C. Rhea
aefc1d18e8 svn ignore libsrm.a 2007-04-12 04:09:00 +00:00
Sean C. Rhea
d7daa3d54d added import from SRM menu item and made cpint not crash when it can't find
cpint data for a ride (until we get it working with srm data)
2007-04-12 04:07:33 +00:00
Sean C. Rhea
3a2a469d58 Will now read, interpret, and include any appropriately named .srm files in
the user's directory.  Ride summary, ride plot, and power histogram all work
fine, but opening the CP intervals graph causes a crash if any such files
exist.  Also need to add a menu item to import .srm files that renames them to
the proper form (date-time.srm).
2007-04-09 20:43:52 +00:00
Sean C. Rhea
f6968f98bf starting to work on SRM code; this commit will decode .srm files 2007-04-09 01:43:40 +00:00
Sean C. Rhea
2729800d42 new release 2007-04-01 19:14:24 +00:00
Sean C. Rhea
8c7dfdeecd new release 2007-04-01 19:12:57 +00:00
Sean C. Rhea
8a3bc4fd44 new release 2007-04-01 19:02:52 +00:00
Sean C. Rhea
3e7d53b418 new release 2007-04-01 18:57:21 +00:00
Sean C. Rhea
0dc0f48416 Hardware echo detection was being fooled because in the normal case, the
result of a version request, triggered by writing 'V', is the string "VER...".
Sometimes, this was preceeded by a zero byte, in which case hardware echo
detection worked, but other times it wasn't, and we stripped the 'V' from
"VER...", only to wait forever for one extra byte that wasn't coming.  The new
approach is to read until we get the "\r\n", then search for "VER", then see
if any other 'V's preceed it, in which case we assume we're dealing with
hardware echo.
2007-04-01 05:41:38 +00:00
Sean C. Rhea
6bee5b3ca0 How did we NOT always open the serial port with O_NONBLOCK? WTF? 2007-04-01 05:07:04 +00:00
Sean C. Rhea
dd9ff12914 patch from Aldy to make wattage an integer in CVS exports 2007-03-07 20:44:49 +00:00
Sean C. Rhea
f18ea0d646 fixed Mac release bugs and updated both feb 21 to feb 22 2007-02-22 17:22:25 +00:00
Sean C. Rhea
54ab7d02c3 fix for centering in QT 4.1.1 2007-02-22 17:12:50 +00:00
Sean C. Rhea
6fc7669b71 cpint picker and rec_int fix, mac version 2007-02-22 03:02:11 +00:00
Sean C. Rhea
4ac73d88ac with picker and rec_int bug fix, linux version 2007-02-22 02:58:31 +00:00
Sean C. Rhea
a341bc4302 using a picker in cpint plot 2007-02-22 02:46:02 +00:00
Sean C. Rhea
1143a30248 fix misinterpretation of rec_int and add error message
for time moving backwards
2007-02-21 17:31:31 +00:00
Sean C. Rhea
04aadf4ee3 with interval seconds padding fix 2007-02-13 02:44:13 +00:00
Sean C. Rhea
f726979a01 add leading zero to single-digit seconds values 2007-02-13 02:40:45 +00:00
Sean C. Rhea
dc1fcfef35 add intervals 2007-02-12 20:58:37 +00:00
Sean C. Rhea
6a121a7fc7 add new release 2007-02-12 20:58:26 +00:00
Sean C. Rhea
c24e341b3d linux version 2007-02-12 20:47:23 +00:00
Sean C. Rhea
9f6a0ad7d6 new release; linux version coming soon 2007-02-12 20:42:58 +00:00
Sean C. Rhea
e9628702ed added Dan Connelly 2007-02-12 20:30:30 +00:00
Sean C. Rhea
a179ec592a return after reject() 2007-02-12 20:25:12 +00:00
Sean C. Rhea
0e09790c5b patch from Aldy to fix interval durations that display as XX:60 2007-02-12 20:16:48 +00:00
Sean C. Rhea
5593c8e3f7 add work, clarify units 2007-02-12 20:14:16 +00:00
Sean C. Rhea
a2f233b345 detect hwecho based on whether the hardware seems to be echoing during
pt_read_version, rather than based on the device name
2007-02-12 03:00:42 +00:00
Sean C. Rhea
a5b621a13d add interval information to ride summary 2007-02-11 20:56:13 +00:00
Sean C. Rhea
2b23610f5f add "Export to CSV..." menu option 2007-02-11 20:00:45 +00:00
Sean C. Rhea
491ef5c639 check for unreasonable speeds, and patches from Aldy 2007-02-11 18:40:14 +00:00
Sean C. Rhea
4283c5b408 fixed bug pointed out by Dan Connelly with graph sliders in empty rides 2007-02-10 04:14:48 +00:00
Sean C. Rhea
fa45b5c980 fixed link 2007-02-01 16:24:57 +00:00
Sean C. Rhea
81c5cf80b3 add new releases 2007-01-30 23:16:51 +00:00
Sean C. Rhea
982fb843cf new mac release
This line, and those below, will be ignored--

AM   doc/GoldenCheetah_2007-01-30_Darwin_powerpc.dmg
2007-01-30 23:10:30 +00:00
Sean C. Rhea
3b23740f62 new linux build 2007-01-30 23:10:00 +00:00
Sean C. Rhea
78fbe4e643 specify QT 4.1.1-static in path 2007-01-30 22:26:50 +00:00
Sean C. Rhea
d2abe9716f Fix for crash where ride time is shorter than smoothing. In particular, the
sample ride is shorter than the default smoothing value.
2007-01-29 19:59:04 +00:00
Sean C. Rhea
6134c947ed patch from Aldy to ignore zeros in cadence and hr and optionally in power 2007-01-24 20:23:41 +00:00
Sean C. Rhea
035226c3b5 fix NaNs in ride summary 2007-01-21 02:57:02 +00:00
Sean C. Rhea
734d025b7f remove Linux tarballs 2007-01-19 18:19:10 +00:00
Sean C. Rhea
e4e6c39aba added download screenshot 2007-01-19 18:16:52 +00:00
Sean C. Rhea
8bd578057f Linux build, more documentation on building from scratch 2007-01-19 18:12:09 +00:00
Sean C. Rhea
c850170cfc bunch of "#include <assert.h>" lines for Andrew Kruse 2007-01-19 17:47:41 +00:00
Sean C. Rhea
7f2d13a779 added link to FTDI drivers 2007-01-19 17:43:11 +00:00
Sean C. Rhea
f628170850 metric and csv options from Aldy 2007-01-19 17:00:47 +00:00
Sean C. Rhea
79b83fd668 another patch from Aldy: must compare to NaN with ne, not != 2007-01-17 22:27:32 +00:00
Sean C. Rhea
861507bb30 linux screenshot 2007-01-06 22:48:45 +00:00
Sean C. Rhea
1de131f122 ignore executable on Linux 2007-01-06 22:31:08 +00:00
174 changed files with 31224 additions and 4301 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# old skool
.svn
# osx noise
.DS_Store
profile

View File

@@ -1,31 +0,0 @@
#
# $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
#
SUBDIRS=doc src
all: subdirs
.PHONY: all subdirs clean
clean:
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir clean; done
subdirs:
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir; done

9
doc/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# old skool
.svn
# osx noise
.DS_Store
profile
#html files are auto-generated by the scripts:
*.html

View File

@@ -4,11 +4,10 @@ HTML=$(subst .content,.html,$(CONTENT))
TARBALLS=$(wildcard gc_*.tgz)
OTHER=logo.jpg sample.gp sample.png cpint.gp cpint.png \
screenshot-summary.png screenshot-plot.png \
screenshot-cpint.png screenshot-phist.png
DISTRIB=GoldenCheetah_2006-12-25_Darwin_powerpc.dmg \
GoldenCheetah_2006-09-06_Darwin_powerpc.dmg \
GoldenCheetah_2006-09-07_Darwin_powerpc.dmg \
GoldenCheetah_2006-09-19_Darwin_powerpc.dmg
screenshot-cpint.png screenshot-phist.png \
screenshot-download.png screenshot-weekly.png \
choose-a-cyclist.png main-window.png critical-power.png \
power.zones
all: $(HTML)
.PHONY: all clean install
@@ -17,8 +16,11 @@ clean:
rm -f $(HTML)
install:
rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) $(DISTRIB) \
srhea.net:public_html/goldencheetah/
rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) \
srhea.net:wwwroot/goldencheetah.org/
command-line.html: command-line.content genpage.pl
./genpage.pl "Legacy Command-Line Tools" $< > $@
contact.html: contact.content genpage.pl
./genpage.pl "Contact Us" $< > $@
@@ -47,3 +49,9 @@ search.html: search.content genpage.pl
users-guide.html: users-guide.content genpage.pl
./genpage.pl "User's Guide" $< > $@
wishlist.html: wishlist.content genpage.pl
./genpage.pl "Wish List" $< > $@
zones.html: zones.content genpage.pl
./genpage.pl "Power Zones File Guide" $< > $@

BIN
doc/cheetah_logo.eps Normal file

Binary file not shown.

BIN
doc/choose-a-cyclist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

232
doc/command-line.content Normal file
View File

@@ -0,0 +1,232 @@
<!-- $Id: users-guide.content,v 1.5 2006/05/27 16:32:46 srhea Exp $ -->
<p>
<big><font face="arial,helvetica,sanserif">
Using the Command Line Utilities
</font></big>
<p>
In addition to the GUI, Golden Cheetah comes with
several command line utilities:
<code>ptdl</code>, which downloads ride data from a PowerTap Pro version 2.21
cycling computer, <code>ptunpk</code>, which unpacks the raw bytes downloaded
by <code>ptdl</code> and outputs more human-friendly ride information, and
<code>cpint</code>, which computes your critical power (see below). We've
also written several Perl scripts to help you graph and summarize the data.
<p>
NOTE: We no longer support the use of the command-line tools. Please use the
graphical version of GoldenCheetah instead. This documentation is here for
the benefit of the brave alone.
<p>
<big><font face="arial,helvetica,sanserif">
Extracting the Data
</font></big>
<p>
First, make sure you have the FTDI drivers installed, as described in the <a
href="users-guide.html">User's Guide</a>. You can then run <code>ptdl</code>
without arguments:
<pre>
$ ./ptdl
Reading from /dev/tty.usbserial-3B1.
Reading version information...done.
Reading ride time...done.
Writing to 2006_05_15_11_34_03.raw.
Reading ride data..............done.
$ head -5 2006_05_15_11_34_03.raw
57 56 55 64 02 15
60 06 05 0f 6b 22
40 08 30 00 00 00
86 0e 74 99 00 55
81 06 77 a8 40 55
</pre>
<p>
If everything goes well, <code>ptdl</code> will automatically detect the
device (<code>/dev/tty.usbserial-3B1</code> 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 (<code>2006_05_15_11_34_03.raw</code> in the example; the format
is YYYY_MM_DD_hh_mm_ss.raw).
<p>
<big><font face="arial,helvetica,sanserif">
Unpacking the Data
</font></big>
<p>As shown by the <code>head</code> command above, the data in this
<code>.raw</code> file is just the raw bytes that represent your ride. To
unpack those bytes and display them in a more human-friendly format, use
<code>ptunpk</code>:
<pre>
$ ./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
</pre>
<code>ptunpk</code> takes a <code>.raw</code> file for input and writes a
<code>.dat</code> 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.
<p>
<big><font face="arial,helvetica,sanserif">
Summarizing the Data
</font></big>
<p>
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
<code>intervals.pl</code> summarizes the intervals performed in a workout:
<small>
<pre>
$ ./intervals.pl 2006_05_03_16_24_04.dat
Power Heart Rate Cadence Speed
Int Dur Dist Avg Max Avg Max Avg Max Avg Max
0 77:10 19.3 213 693 134 167 82 141 16.0 27.8
1 4:03 0.9 433 728 175 203 84 122 13.0 18.8
2 7:23 1.0 86 502 135 179 71 141 16.0 28.2
3 4:27 0.9 390 628 170 181 70 100 12.0 17.6
4 8:04 0.9 60 203 130 178 50 120 18.0 30.1
5 4:30 0.9 384 682 170 179 79 113 11.0 18.6
6 8:51 1.1 53 245 125 176 70 141 8.0 26.6
7 2:48 0.4 400 614 164 178 62 91 8.0 13.6
8 7:01 1.1 46 268 128 170 71 141 12.0 28.8
9 4:30 0.9 379 560 168 180 81 170 11.0 18.3
10 28:46 6.5 120 409 128 179 79 141 15.0 31.0
</pre>
</small>
<p>
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).
<p>
<big><font face="arial,helvetica,sanserif">
Graphing the Data
</font></big>
<p>
For graphing the data in the ride, we use <code>smooth.pl</code> and the
<code>gnuplot</code> program. You can use <a href="sample.gp">sample.gp</a>
to graph the power, heart rate, cadence, and speed for the hill workout above:
<pre>
$ gnuplot sample.gp
</pre>
<img align="center" alt="Sample Plot" src="sample.png">
<p>
<big><font face="arial,helvetica,sanserif">
Finding Your "Critical Power"
</font></big>
<p>
Joe Friel calls the maximum average power a rider can sustain over an interval
the rider's "critical power" for that duration. The <code>cpint</code>
program automatically computes your critical power over all interval lengths
using the data from all your past rides. This program looks at all the
<code>.raw</code> files in a directory, calculating your maximum power over
every subinterval length and storing them in a corresponding <code>.cpi</code>
file. It then combines the data in all of the <code>.cpi</code> files to find
your critical power over <i>all</i> subintervals of <i>all</i> your rides.
<pre>
$ 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
</pre>
<p>
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.
<p>
We can graph the output of <code>cpint</code> using <code>gnuplot</code> with
<a href="cpint.gp">cpint.gp</a>:
<pre>
$ ./cpint > cpint.out
$ gnuplot cpint.gp
</pre>
<img src="cpint.png">
<p>
The first time you run <code>cpint</code> it will take a while, as it has to
analyze all your past rides. On subsequent runs, however, it will only
analyze new files.
<p><i>Training and Racing with a Power Meter</i> (see the <a
href="faq.html">FAQ</a>) 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 <code>cpint</code> program,
you can determine whether you're stronger than others in your racing category
at each interval length and adapt your training program accordingly.
<p>
<big><font face="arial,helvetica,sanserif">
Converting Old Data
</font></big>
<p>
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 <code>.xml</code> files that
PowerTuned produces to <code>.raw</code> files using the <code>ptpk</code>
program:
<p>
<pre>
$ ./ptpk 2006_04_27_00_23_28.xml
$ head -5 2006_04_27_00_23_28.raw
57 56 55 64 02 15
60 06 04 7b 80 17
40 08 30 00 00 00
84 04 00 24 00 ff
83 03 00 d7 00 ff
</pre>
<p>
<code>ptpk</code> assumes the input <code>.xml</code> 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 <code>-w</code> and
<code>-r</code> options.
<p>
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 <code>.xml</code> 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 <code>ptpk</code>,
<code>ptunpk</code> is not quite the identity function; in particular, the
wattage values from <code>ptpk</code> may only be accurate to two significant
digits.

View File

@@ -11,8 +11,22 @@ used, and Sean Rhea coded up their combined discoveries into the two
utilities, <code>ptdl</code> and <code>ptunpk</code>.
<p>
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 <code>/dev/cu.*</code> devices instead of the
<code>/dev/tty.*</code> ones.
Later that year, Sean needed to learn QT for his real job, and he set about
writing a graphical version of <code>ptdl</code> and <code>ptunpk</code> for
practice. He released the first graphical version on September 6, 2006,
changing the name to GoldenCheetah in reference to an old legend from his days
as a runner.
<p>
Since then, a number of others have helped out in various ways.
Robert Carlsen helped get the serial port version of the PowerTap Pro working
with the Keyspan USB-to-serial adaptor. Robert also figured out how to build
universal binaries for Mac OS X. Scott Overfield helped figure out
that we should be using the <code>/dev/cu.*</code> devices instead of the
<code>/dev/tty.*</code> ones. Aldy Hernandez and Andrew Kruse helped get
things working under Linux.
Dan Connelly helped find and fix several core dumps.
Justin Knotzke contributed code to import comma-separated value files
and visually mark intervals on the ride plot. J.T. Conklin added the ability
to import TCX files. J.T. also added a pedal force vs. pedal velocity chart. Tom Montgomery added IBike 3 support and cleaned up the Import CSV tool, adding the default ride date when importing new files. Ned Harding resurrected the Windows builds of Golden Cheetah (fixing USB download inconsistencies in the process), then added slew of functions including the long-desired Split Ride feature. (available in version 1.0.305+).

BIN
doc/critical-power.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@@ -1,7 +1,14 @@
<!-- $Id: download.content,v 1.6 2006/08/11 20:21:03 srhea Exp $ -->
<!-- $Id: download.content,v 1.6 2009/01/09 20:45:03 rcarlsen Exp $ -->
Right now Golden Cheetah is available as source code and in binary form for
Mac OS X on PowerPC processors.
<p>
Golden Cheetah is available as source code and in binary form for
Mac OS X Universal Binary, Linux on x86 processors and Windows 32-bit.
</p>
<p>
Depending on your operating system, you may need to install the <a
href="http://www.ftdichip.com/Drivers/D2XX.htm">FTDI USB
driver</a> if you're using the PowerTap's new USB download cradle. The FTDI USB drivers are an optional install if you do not plan on downloading from your device using Golden Cheetah.
</p>
<p>
<font face="arial,helvetica,sanserif">
@@ -10,15 +17,20 @@ Mac OS X on PowerPC processors.
<p>
The Golden Cheetah source code is available via
<a href="http://subversion.tigris.org/">Subversion</a>.
<a href="http://git-scm.com/">git</a>. You need to install
<a href="http://www.trolltech.com/products/qt">QT 4.5.x</a> and
<a href="http://qwt.sourceforge.net/">qwt 5.0.2</a> (as a static, not
dynamic, library) and
edit <code>goldencheetah/src/src.pro</code> to point to them
before building GoldenCheetah.
Use this command to check out the current version of the repository:
<pre>
svn checkout http://goldencheetah.org/svn/trunk goldencheetah
git clone git://github.com/srhea/GoldenCheetah.git
</pre>
You can also browse the source <a
href="http://goldencheetah.org/svn/trunk">here</a>.
You can also <a href="http://github.com/srhea/GoldenCheetah/tree/master/">browse
the source on github</a>.
<p>
<font face="arial,helvetica,sanserif">
@@ -27,13 +39,178 @@ href="http://goldencheetah.org/svn/trunk">here</a>.
<p>
<center>
<table width="100%">
<table width="100%" cellspacing="10">
<tr>
<td width="20%"><i>Date</i></td>
<td width="30%"><i>File</i></td>
<td width="20%"><i>Version</i></td>
<td width="30%"><i>Files</i></td>
<td><i>Description</i></td>
</tr>
<tr>
<td valign="top">1.1.325</td>
<td valign="top">
<a href="GoldenCheetah_1.1.325_Linux_x86.gz">Linux x86</a><br>
<a href="GoldenCheetah_1.1.325_Linux_x86_64.gz">Linux x86_64</a><br>
<a href="GoldenCheetah_1.1.325_Darwin_Universal.dmg">Mac OS X Universal</a><br>
<a href="GoldenCheetah_1.1.325_Windows_Installer.exe">Windows 32-bit</a>
</td>
<td valign="top">
<p>
First official Windows release courtesy of Ned Harding. Ned put much effort into the port to make the download reliable and created a nice installer, too (Thanks Ned!). He also provided the long-awaited Split Ride feature - break up a ride file into separate rides easily using long time gaps and intervals.
<ul>
<li>Ant+Sport PowerTap support.
<li>Split Rides by time gaps or intervals.
<li>Delete ride from list.
<li>Use distance or time for x-axis in Ride Plot (Thanks Damain).
<li>Numerous bug fixes (Thanks Tom, Dan).
</p>
</td>
</tr>
<tr>
<td valign="top">1.0.277</td>
<td valign="top">
<a href="GoldenCheetah_1.0.277_Linux_x86.gz">Linux x86</a>,<br>
<a href="GoldenCheetah_1.0.277_Linux_x86_64.tar.gz">Linux x86_64</a>,<br>
<a href="GoldenCheetah_1.0.277_Darwin_Universal.dmg">Mac OS X Universal</a>
</td>
<td valign="top">
<p>*Note: Beginning with this release we are changing to a numbered versioning system. Minor point releases will generally indicate builds with new features, while bugfix releases will increment the final number, which represents the svn revision*</p>
<p>Several new features in this release: Critical Power calculator, find best intervals utility, Pedal Force / Pedal Velocity chart, iBike and Ergomo CSV import, GUI power zones creator, separate vertical axes for Power / HR / Cadence and Speed in the Ride plot, sorting rides with the most recent at the top of the list, and many bug fixes courtesy of JT Conklin.
</p>
<p>You may need to install <a href="http://www.ftdichip.com/Drivers/D2XX.htm">USB drivers</a> from FTDI.
</p>
<p>
For posterity, the <a href="http://robertcarlsen.net/blog/?page_id=49">beta version</a> for Windows, based on r295.
</p>
</td>
</tr>
<tr>
<td valign="top">Mar 10, 2008</td>
<td valign="top">
<a href="GoldenCheetah_2008-03-10_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2008-03-10_Darwin_Universal.dmg">Mac OS X Universal</a>
</td>
<td valign="top">
This release introduces <a href="http://www.physfarm.com/Analysis%20of%20Power%20Output%20and%20Training%20Stress%20in%20Cyclists-%20BikeScore.pdf">BikeScore&#8482;</a>,
a metric of training stress developed by Dr. Philip Skiba. It also
fixes several small bugs in earlier releases.
</td>
</tr>
<tr>
<td valign="top">Sep 23, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-09-23_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-09-23_Darwin_i386.dmg">Mac OS X x86</a>,<br>
<a href="GoldenCheetah_2007-09-23_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">
Bug fix release. CVS imports weren't quite working in the last one.
</td>
</tr>
<tr>
<td valign="top">Sep 18, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-09-18_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-09-18_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">
This release adds two small, but excellent features from Justin Knotzke:
CSV file imports and visual interval markers in the ride plot.
</td>
</tr>
<tr>
<td valign="top">Aug 7, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-08-07_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-08-07_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">This release fixes a bug in the critical power
intervals graph where you could get bad data if you started an interval
after a long period of not moving. It also adds really basic zooming to
the ride plot: use the left mouse button to zoom in and the right one to
return to the previous zoom state. It's pretty crappy right now, but
it's better than nothing.
</td>
</tr>
<tr>
<td valign="top">Apr 26, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-04-26_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-04-26_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">This release fixes some bugs and adds a whole bunch of
new features:
<ul>
<li>Now imports .srm files (direct download from SRM hopefully coming
soon)
<li>New "Weekly Summary" tab shows total weekly hours, miles, and work
<li>Power zones can now be entered into a text file, after which GC will
display time in each zone in the ride and weekly summaries; for more
information on the zone file format, <a href="zones.html">see this
page</a>.
</ul>
</td>
</tr>
<tr>
<td valign="top">Apr 1, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-04-01_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-04-01_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">This release fixes a bug that was introduced with the
hardware echo detection code. If you're using the CycleOps-supplied USB
cable to download from your PowerTap unit, this release should make
downloads more reliable. (Those using the KeySpan USB-to-serial adaptor
or a plain-old serial port shouldn't see any difference.)</td>
</tr>
<tr>
<td valign="top">Feb 22, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-02-22_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-02-22_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">Clicking on the Critical Power Plot now displays the
interval duration, maximum power for that ride, and maximum power for all
rides below the plot. Also fixes a bug for recording intervals longer than
two seconds.</td>
</tr>
<tr>
<td valign="top">Feb 12, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-02-12_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-02-12_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">Interval information now included in ride summary, rides can
now be exported as comma-separated values for import into Excel, and better
automatic detection of hardware echo. Also includes a number of bux
fixes.</td>
</tr>
<tr>
<td valign="top">Jan 30, 2007</td>
<td valign="top">
<a href="GoldenCheetah_2007-01-30_Linux_x86.tgz">Linux x86</a>,<br>
<a href="GoldenCheetah_2007-01-30_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
</td>
<td valign="top">Bug fix release.</td>
</tr>
<tr>
<td valign="top">Jan 6, 2007</td>
<td valign="top"><a href="GoldenCheetah_2007-01-06_Linux_x86.tgz">Linux
x86</a></td>
<td valign="top">First release for Linux.</td>
</tr>
<tr>
<td valign="top">Dec 25, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-12-25_Darwin_powerpc.dmg">Mac OS
@@ -84,7 +261,7 @@ These are the older, source-only, command-line distributions. I've left them
up for historical purposes only; I don't recommend using them.
<center>
<table width="100%">
<table width="100%" cellspacing="10">
<tr>
<td width="20%"><i>Date</i></td>
<td width="30%"><i>File</i></td>

View File

@@ -1,8 +1,21 @@
<!-- $Id: faq.content,v 1.4 2006/07/05 16:59:56 srhea Exp $ -->
<p>
<i>I downloaded a .dmg, opened it, dragged and dropped GoldenCheetah into
Applications, double-clicked on it, and nothing happened. What gives?</i>
<b><i>GoldenCheetah doesn't find my PowerTap on Ubuntu Linux.</i></b>
<p>
If you're using the USB cradle (as opposed to the older, serial one),
the FTDI driver sometimes conflicts with the braille terminal in the
default Ubuntu installation. Try unplugging the PT cradle from the
computer and uninstalling <code>brltty</code>:
<blockquote>
<code>sudo apt-get remove brltty</code>
</blockquote>
Then plug the device back in and it should work.
<p>
<b><i>I downloaded a .dmg, opened it, dragged and dropped GoldenCheetah into
Applications, double-clicked on it, and nothing happened. What
gives?</i></b>
<p>
Are you running OS X Tiger? You need to be. If you are, and you're still
@@ -17,7 +30,8 @@ then press &lt;return&gt; and send an email to the mailing list with
whatever it prints out. We'll help you debug it.
<p>
<i>I've downloaded and unpacked the data. Now what do I do with it?</i>
<b><i>I've downloaded and unpacked the data. Now what do I do with
it?</i></b>
<p>
We highly recommend that you buy and read both Joe Friel's <i>The
@@ -44,8 +58,8 @@ marginheight="0" frameborder="0"></iframe>
</center>
<p>
<i>Does the output of <code>ptunpk</code> exactly match that of the software
included with the PowerTap?</i>
<b><i>Does the output of <code>ptunpk</code> exactly match that of the software
included with the PowerTap?</i></b>
<p>
Almost. If you run it in compatibility mode, using the <code>-c</code>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -16,7 +16,7 @@ open (FILE, "$content_file") or die "Could not open $content_file";
print<<EOF;
<!--
Copyright (c) 2006 Sean C. Rhea (srhea\@srhea.net)
Copyright (c) 2006-2008 Sean C. Rhea (srhea\@srhea.net)
All rights reserved.
This file was automatically generated by genpage.pl. To change it,
@@ -26,8 +26,8 @@ print<<EOF;
<html>
<head>
<title>Golden Cheetah: PowerTap Software for Mac OS X</title>
<meta name="keywords" content="powertap mac cycling performance">
<title>Golden Cheetah: Cycling Performance Software for Linux, Mac OS X, and Windows</title>
<meta name="keywords" content="powertap srm linux mac cycling performance">
</head>
<body text="#000000"
@@ -48,6 +48,7 @@ print<<EOF;
<br> <b><a href="screenshots.html">Screenshots</a>
<br> <b><a href="users-guide.html">User's Guide</a>
<br> <b><a href="faq.html">FAQ</a>
<br> <b><a href="wishlist.html">Wish List</a>
<br> <b><a href="license.html">License</a></b>
<br> <b><a href="download.html">Download</a></b>
<br> <b><a href="contrib.html">Contributors</a></b>
@@ -87,7 +88,7 @@ google_color_text = "000000";
Cheetah</font></b></big></big></big>
<br>
<big><font face="arial,helvetica,sanserif">
PowerTap Software for Mac OS X
Cycling Performance Software for Linux, Mac OS X, and Windows
</font></big>
<p>
</td></tr>

View File

@@ -1,26 +1,26 @@
<!-- $Id: index.content,v 1.1 2006/05/16 14:24:50 srhea Exp $ -->
<p>
The goal of the Golden Cheetah project is to develop a software package that:
GoldenCheetah is a software package that:
<ul>
<li>Downloads ride data from power measurement devices, such as the <a
href="http://www.cycleops.com/products/powertap.htm">CycleOps PowerTap</a>,
the <a href="http://www.ergomo.net/Home-_14.html">ergomo</a>, the <a
href="http://www.polarusa.com/consumer/powerkit/default.asp">Polar
Electro</a>, and the <a href="http://www.srm.de/usa/index.html">SRM Training
System</a><p>
<li>Helps athletes analyze downloaded data with features akin to commercial
power analysis software, such as <a href="http://cyclingpeaks.com/">Cycling
Peaks</a><p>
<li>Works on non-Microsoft Windows-based systems, such as FreeBSD, Linux, and
Mac OS X<p>
<li>Is available under an
<a href="http://www.opensource.org/docs/definition.php">Open Source</a>
license
<li>Downloads ride data directly from the CycleOps PowerTap, including the
newer Ant+Sport models.<p>
<li>Imports ride data from SRM, Garmin TCX, Polar HRM, and CSV files,
including those from Cycling Peaks&nbsp;(TM) and the ergomo.<p>
<li>Provides a rich set of analysis tools, including critial power,
BikeScore&nbsp;(TM), power histograms, a best interval finder, and pedal force
versus pedal velocity, to name just a few.<p>
<li>Works identically under Linux, Mac OS X, and Windows.<p>
<li>Is available under an Open Source license.
</ul>
<p>
In short, we believe that cyclists should be able to download their power data
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.

BIN
doc/main-window.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@@ -1,5 +1,6 @@
#!/bin/sh
# $Id: mkdmg.sh,v 1.2 2006/09/06 23:23:03 srhea Exp $
export PATH=/usr/local/Trolltech/Qt-4.1.1-static/bin:$PATH
VERS=`date +'%Y-%m-%d'`
OS=`uname -s`
CPU=`uname -p`
@@ -7,22 +8,24 @@ SUFFIX="$VERS"_"$OS"_"$CPU"
rm -rf tmp
mkdir tmp
cd tmp
svn checkout svn+ssh://goldencheetah.org/home/srhea/svnroot/goldencheetah/trunk goldencheetah
svn checkout svn+ssh://goldencheetah.org/home/srhea/svnroot/goldencheetah/trunk/src goldencheetah
cd goldencheetah
qmake
make
mv src/gui/GoldenCheetah.app ..
make clean
rm doc/gc_*.tgz
rm doc/GoldenCheetah_*.dmg
mv gui/GoldenCheetah.app ..
#make clean
#rm doc/gc_*.tgz
#rm doc/GoldenCheetah_*.dmg
#rm doc/GoldenCheetah_*.tgz
cd ..
strip GoldenCheetah.app/Contents/MacOS/GoldenCheetah
find . -name .svn | xargs rm -rf
tar czvf src.tgz goldencheetah
rm -rf goldencheetah
SIZE=`du -csk * | grep total | awk '{printf "%.0fm", $1/1024+5}'`
strip GoldenCheetah.app/Contents/MacOS/GoldenCheetah
#find . -name .svn | xargs rm -rf
#tar czvf src.tgz goldencheetah
SIZE=`du -csk GoldenCheetah.app | grep total | awk '{printf "%.0fm", $1/1024+5}'`
hdiutil create -size $SIZE -fs HFS+ -volname "Golden Cheetah $VERS" tmp.dmg
hdiutil attach tmp.dmg
cp -R GoldenCheetah.app src.tgz /Volumes/Golden\ Cheetah\ $VERS/
cp -R GoldenCheetah.app /Volumes/Golden\ Cheetah\ $VERS/
hdiutil detach /Volumes/Golden\ Cheetah\ $VERS/
hdiutil convert tmp.dmg -format UDZO -o GoldenCheetah_$SUFFIX.dmg
hdiutil internet-enable -yes GoldenCheetah_$SUFFIX.dmg

27
doc/power.zones Normal file
View File

@@ -0,0 +1,27 @@
From BEGIN until 2006/07/17, CP=297:
1, Active Recovery, 122, 167
2, Endurance, 167, 228
3, Tempo, 228, 274
4, Lactate Threshold, 274, 319
5, VO2 Max, 319, 365
6, Anaerobic Capacity, 365, 678
7, Sprinting, 678, MAX
From 2006/07/17 until 2007/02/05, CP=329:
1, Active Recovery, 135, 185
2, Endurance, 185, 253
3, Tempo, 253, 303
4, Lactate Threshold, 303, 354
5, VO2 Max, 354, 404
6, Anaerobic Capacity, 404, 752
7, Sprinting, 752, MAX
From 2007/02/05 until END, CP=347:
1, Active Recovery, 139, 191
2, Endurance, 191, 260
3, Tempo, 260, 312
4, Lactate Threshold, 312, 364
5, VO2 Max, 364, 416
6, Anaerobic Capacity, 416, 774
7, Sprinting, 774, MAX

BIN
doc/screenshot-download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 81 KiB

BIN
doc/screenshot-weekly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -3,6 +3,13 @@
<p>
<center>
<big><font face="arial,helvetica,sanserif">
The Download Dialog
</font></big>
<p>
<img src="screenshot-download.png" alt="Download Dialog" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
The Ride Summary
</font></big>
@@ -30,6 +37,13 @@ The Power Histogram
<p>
<img src="screenshot-phist.png" alt="The Power Histogram" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
The Weekly Summary
</font></big>
<p>
<img src="screenshot-weekly.png" alt="The Weekly Summary" align="center">
</center>

View File

@@ -1,241 +1,233 @@
<!-- $Id: users-guide.content,v 1.5 2006/05/27 16:32:46 srhea Exp $ -->
<big><font face="arial,helvetica,sanserif">
Using the GUI
Step 1: Installing the FTDI drivers
</font></big>
<p>
Using the graphical version of Golden Cheetah should be pretty
self-explanatory. Download the disk image from the <a
href="download.html">download page</a>, drag the Golden Cheetah application
into your Applications folder, open your Applications folder, and then double
click on Golden Cheetah.
Depending on your operating system, you may need to install the <a
href="http://www.ftdichip.com/Drivers/D2XX.htm">FTDI USB
driver</a> if you're using the PowerTap's new USB download cradle. The FTDI USB drivers are an optional install if you do not plan on downloading from your device using Golden Cheetah.
</p>
<p>
If you're running Linux, you may also need to uninstall the <code>brtty</code>
(Braille TTY) application, as it interferes with FTDI's driver. The command
<pre>
sudo apt-get remove brtty
</pre>
should do the trick on Debian/Ubuntu.
<p>
The latest version (7.1.1) of Saris's PowerAgent software uses an incompatible
version of FTDI's driver from the one GoldenCheetah uses, and PowerAgent
removes the driver that GoldenCheetah needs when you install PowerAgent. If
you want to run both GoldenCheetah and PowerAgent, you need to use PowerAgent
version 7.0.1 or earlier. We're working to correct this problem, but we're
not there yet.
<p>
<big><font face="arial,helvetica,sanserif">
Using the Command Line Utilities
Step 2: Installing GoldenCheetah
</font></big>
<p>
In addition to the GUI, Golden Cheetah comes with
several command line utilities:
<code>ptdl</code>, which downloads ride data from a PowerTap Pro version 2.21
cycling computer, <code>ptunpk</code>, which unpacks the raw bytes downloaded
by <code>ptdl</code> and outputs more human-friendly ride information, and
<code>cpint</code>, 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.
To install GoldenCheetah, go to <a href="download.html">the download page</a>
and download the version for your operating system and processor.
<p>
On Mac OS X, when the download finishes, Mac OS X should automatically open
the <code>.dmg</code> file for you. If not, double-click to open it. Drag
the GoldenCheetah icon into your Applications folder, and you're done.
<p>
The Linux version of GoldenCheetah is distributed as a tarball. Download this
file and save it to <code>/tmp</code>, then from a terminal:
<pre>
cd /tmp
tar xzvf GoldenCheetah_DATE_Linux_x86.tgz
cd GoldenCheetah_DATE_Linux_x86
sudo cp GoldenCheetah /usr/local/bin
cd ..
rm -rf GoldenCheetah_DATE_Linux_x86.tgz
</pre>
Be sure to replace "DATE" with the date of the revision you downloaded, such
as "2007-09-23".
<p>
<big><font face="arial,helvetica,sanserif">
Extracting the Data
Step 3: Running GoldenCheetah
</font></big>
<p>
To use <code>ptdl</code>, you'll first need to install
<a href="http://www.ftdichip.com/Drivers/VCP.htm">the drivers</a> for the
FTDI chip the PowerTap Pro USB Downloader uses. Once these are installed, you
should be able to just run <code>ptdl</code> without arguments:
<pre>
$ ./ptdl
Reading from /dev/tty.usbserial-3B1.
Reading version information...done.
Reading ride time...done.
Writing to 2006_05_15_11_34_03.raw.
Reading ride data..............done.
$ head -5 2006_05_15_11_34_03.raw
57 56 55 64 02 15
60 06 05 0f 6b 22
40 08 30 00 00 00
86 0e 74 99 00 55
81 06 77 a8 40 55
</pre>
<p>
If everything goes well, <code>ptdl</code> will automatically detect the
device (<code>/dev/tty.usbserial-3B1</code> 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 (<code>2006_05_15_11_34_03.raw</code> in the example; the format
is YYYY_MM_DD_hh_mm_ss.raw).
To run GoldenCheetah on Mac OS X, double-click on the GoldenCheetah icon in
your Applications folder. On Linux, just type "GoldenCheetah" at the prompt.
<p>
The first time you run GoldenCheetah, you'll get an empty "Choose a Cyclist"
dialog box:
<p>
<center><img src="choose-a-cyclist.png"></center>
<p>
Click on "New...", enter your name and click "OK", then select your name and
click "Open". After that, the main GoldenCheetah window will open:
<p>
<center><img src="main-window.png"></center>
<p>
Your main window won't yet have any rides in it, of course. To fix that, you
need either to download a ride from your PowerTap or import one from another
program. GoldenCheetah can import <code>.srm</code> files recorded on SRM
power meters and <code>.csv</code> files created by other programs. To
download a file from your PowerTap, select "Ride->Download from device..."
from the menu. To import one, select either "Ride->Import from SRM..." or
"Ride->Import from CSV...".
<p>
Once you've downloaded or imported a ride, you can see some simple statistics
about it on the "Ride Summary" page: your total riding time and average power,
for example. If you click on the "Ride Plot" tab at the top of the screen,
you can see a graph of your speed, power, cadence, and heart rate during the
ride. The "Power Histogram" shows how much time you spent at each power
during the ride, and the "Notes" tab allows you to record notes about the
ride. The "Weekly Summary" shows your total time and work for the week.
<p>
The "Critical Power Plot" is one of the most useful features of GoldenCheetah.
It shows the highest average power you attained for every interval length
during the ride. Some people call this the "Mean Maximal Power" graph. The
green line shows values for this ride; the red line shows the combination of
all your rides. (If you only have one ride so far, the two lines will
overlap.) Clicking on the graph with your mouse brings up a blue line, and
the values under this line are shown at the bottom of the screen.
<p>
It helps to think about an example:
<p>
<center><img src="critical-power.png"></center>
<p>
In this example, the blue line is right around the 14-second mark on the
x-axis. So the values shown under "Today" and "All Rides", at the bottom, are
the hardest the cyclist went for any 14-second period during the ride itself
and during all rides he's ever recorded in GoldenCheetah. Since the two
values are the same, he set a new personal record during this ride.
<p>
The Critical Power Plot is most useful before you're going to go do intervals
or a time trial. Say you want to do six 2-minute intervals with three minutes
rest in between. Click on the Critical Power Plot, drag the blue line to the
2-minute mark, and read the value shown in "All Rides". That's the hardest
you've ever gone for two minutes. Now go out and try to beat it!
<p>
<big><font face="arial,helvetica,sanserif">
Unpacking the Data
Step 4: Setting Up Your Power Zones
</font></big>
<p>As shown by the <code>head</code> command above, the data in this
<code>.raw</code> file is just the raw bytes that represent your ride. To
unpack those bytes and display them in a more human-friendly format, use
<code>ptunpk</code>:
<p>
If you look back at the screenshot above, you may notice that there are
several things shown in the "Ride Summary" tab that aren't on your version.
The picture above shows a non-zero "Bike Score", and there's a list of how
much time the cyclist spent in each "Power Zone" during the ride as well.
<p>
BikeScore(TM) is a measure of the physiological stress you underwent during a
ride. It was developed by Dr. Philip Skiba, and you can read more about it in
<a href="http://www.physfarm.com/Analysis%20of%20Power%20Output%20and%20Training%20Stress%20in%20Cyclists-%20BikeScore.pdf">an article he wrote</a>.
<p>
For GoldenCheetah to compute your BikeScore and the time spent in each power
zone, you first need to tell it what your power zones and critical power
are. You can define your power zones however you like, maybe using the ones
defined by Joe Friel, for example. Your critical power should be the
maximum power you can sustain over an hour. Some people call this your
"lactate threshold" or "functional threshold power". Our friend Bill says a
rose by any other name would smell as sweet.
<p>
We'll have a dialog box that will let you set up your power zones and
critical power in a future version of GoldenCheetah, but for now you'll need
to use a text editor. On Linux, that probably means nano, vi, or emacs.
On Mac, the easiest editor to use is TextEdit, which is in your Applications
folder.
<p>
Start by
downloading <a href="power.zones">this sample file</a> and saving it in
<pre>
$ ./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
~/Library/GoldenCheetah/Your Name/power.zones
</pre>
<code>ptunpk</code> takes a <code>.raw</code> file for input and writes a
<code>.dat</code> 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.
<p>
where "~" is your home directory (e.g., <code>/Users/srhea</code> on Mac or
<code>/home/srhea</code> on Linux) and "Your Name" is the name you chose when
you first opened GoldenCheetah. Open the power.zones file in a text editor
and you'll see this:
<blockquote>
<pre>
From BEGIN until 2006/07/17, CP=297:
1, Active Recovery, 122, 167
2, Endurance, 167, 228
3, Tempo, 228, 274
4, Lactate Threshold, 274, 319
5, VO2 Max, 319, 365
6, Anaerobic Capacity, 365, 678
7, Sprinting, 678, MAX
From 2006/07/17 until 2007/02/05, CP=329:
1, Active Recovery, 135, 185
2, Endurance, 185, 253
3, Tempo, 253, 303
4, Lactate Threshold, 303, 354
5, VO2 Max, 354, 404
6, Anaerobic Capacity, 404, 752
7, Sprinting, 752, MAX
From 2007/02/05 until END, CP=347:
1, Active Recovery, 139, 191
2, Endurance, 191, 260
3, Tempo, 260, 312
4, Lactate Threshold, 312, 364
5, VO2 Max, 364, 416
6, Anaerobic Capacity, 416, 774
7, Sprinting, 774, MAX
</pre>
</blockquote>
<p>
The format of the file is simple. You define a range of time, starting with a
date or "BEGIN" to indicate the oldest possible time and ending with a date or
"END" to indicate the latest possible time. Then you put your critical power
(CP) for that date range. Then you list your zones, where each zone has a
number, a name, a minimum power value, and a maximum power value. You can
have as many time ranges and zones as you like. Most people enter a new time
range every time their critical power goes up--right after a fitness test, for
example.
<p>
NOTE: By default, Mac OS's TextEdit will try and save the power.zones file
with a <code>.txt</code> extension. Use the menu command "Format->Make Plain
Text" to get it to let you save the file with a <code>.zones</code> extension
instead.
<p>
<big><font face="arial,helvetica,sanserif">
Summarizing the Data
</font></big>
<p>
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
<code>intervals.pl</code> summarizes the intervals performed in a workout:
<small>
<pre>
$ ./intervals.pl 2006_05_03_16_24_04.dat
Power Heart Rate Cadence Speed
Int Dur Dist Avg Max Avg Max Avg Max Avg Max
0 77:10 19.3 213 693 134 167 82 141 16.0 27.8
1 4:03 0.9 433 728 175 203 84 122 13.0 18.8
2 7:23 1.0 86 502 135 179 71 141 16.0 28.2
3 4:27 0.9 390 628 170 181 70 100 12.0 17.6
4 8:04 0.9 60 203 130 178 50 120 18.0 30.1
5 4:30 0.9 384 682 170 179 79 113 11.0 18.6
6 8:51 1.1 53 245 125 176 70 141 8.0 26.6
7 2:48 0.4 400 614 164 178 62 91 8.0 13.6
8 7:01 1.1 46 268 128 170 71 141 12.0 28.8
9 4:30 0.9 379 560 168 180 81 170 11.0 18.3
10 28:46 6.5 120 409 128 179 79 141 15.0 31.0
</pre>
</small>
<p>
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).
<p>
<big><font face="arial,helvetica,sanserif">
Graphing the Data
</font></big>
<p>
For graphing the data in the ride, we use <code>smooth.pl</code> and the
<code>gnuplot</code> program. You can use <a href="sample.gp">sample.gp</a>
to graph the power, heart rate, cadence, and speed for the hill workout above:
<pre>
$ gnuplot sample.gp
</pre>
<img align="center" alt="Sample Plot" src="sample.png">
<p>
<big><font face="arial,helvetica,sanserif">
Finding Your "Critical Power"
</font></big>
<p>
Joe Friel calls the maximum average power a rider can sustain over an interval
the rider's "critical power" for that duration. The <code>cpint</code>
program automatically computes your critical power over all interval lengths
using the data from all your past rides. This program looks at all the
<code>.raw</code> files in a directory, calculating your maximum power over
every subinterval length and storing them in a corresponding <code>.cpi</code>
file. It then combines the data in all of the <code>.cpi</code> files to find
your critical power over <i>all</i> subintervals of <i>all</i> your rides.
<pre>
$ 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
</pre>
<p>
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.
<p>
We can graph the output of <code>cpint</code> using <code>gnuplot</code> with
<a href="cpint.gp">cpint.gp</a>:
<pre>
$ ./cpint > cpint.out
$ gnuplot cpint.gp
</pre>
<img src="cpint.png">
<p>
The first time you run <code>cpint</code> it will take a while, as it has to
analyze all your past rides. On subsequent runs, however, it will only
analyze new files.
<p><i>Training and Racing with a Power Meter</i> (see the <a
href="faq.html">FAQ</a>) 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 <code>cpint</code> program,
you can determine whether you're stronger than others in your racing category
at each interval length and adapt your training program accordingly.
<p>
<big><font face="arial,helvetica,sanserif">
Converting Old Data
Legacy Command-Line Tools
</font></big>
<p>
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 <code>.xml</code> files that
PowerTuned produces to <code>.raw</code> files using the <code>ptpk</code>
program:
<p>
<pre>
$ ./ptpk 2006_04_27_00_23_28.xml
$ head -5 2006_04_27_00_23_28.raw
57 56 55 64 02 15
60 06 04 7b 80 17
40 08 30 00 00 00
84 04 00 24 00 ff
83 03 00 d7 00 ff
</pre>
<p>
<code>ptpk</code> assumes the input <code>.xml</code> 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 <code>-w</code> and
<code>-r</code> options.
<p>
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 <code>.xml</code> 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 <code>ptpk</code>,
<code>ptunpk</code> is not quite the identity function; in particular, the
wattage values from <code>ptpk</code> may only be accurate to two significant
digits.
You can still build the older, command-line tools from the source code, but we
no longer include them in releases. <a href="command-line.html">You can find
documentation for them here.</a>

29
doc/wishlist.content Normal file
View File

@@ -0,0 +1,29 @@
<big><font face="arial,helvetica,sanserif">
I wish GoldenCheetah would let me...
</font></big>
<ul>
<li>Split one ride into multiple rides</li>
<li>Download directly from SRM</li>
<li>Graph ride metrics (daily hours, work, BikeScore) over the long
term (weeks, seasons)</li>
<li>Automatically calculate CP from .cpi files</li>
<li>Display the numbers at the bottom of the ride plot, like the
critical power graph does</li>
<li>Select intervals in the ride plot and display metrics for them
at the bottom</li>
<li>Pop up an "importing rides" thermometer when importing</li>
<li>Remember last settings for showPower, showHr, etc., in ride plot</li>
<li>Switch ride plot x-axis from time to distance</li>
<li>Edit power zones in a dialog</li>
<li>-done- Sort rides in AllRides newest to oldest</li>
<li>Group rides list into seasons</li>
<li>Group rides list by type, course</li>
<li>Add lines to CP plot for seasons, last six (eight?) weeks, etc.</li>
<li>Create new intervals</li>
<li>Ignore zeros in power histogram</li>
<li>Show mulitple rides (seasons, etc.) in power histogram</li>
<li>Annotate ride plot</li>
<li>Label rides by type, course</li>
<li>Use the current date as default when importing CSV files</li>
</ul>

69
doc/zones.content Normal file
View File

@@ -0,0 +1,69 @@
<!-- $Id: download.content,v 1.6 2006/08/11 20:21:03 srhea Exp $ -->
The zone file format consists of a list of date ranges and the power
zones that should be used for all rides within each range. Someday I'll
add a dialog to GC that allows you to type in your zones within the
application. Right now, you'll have to write them into a text file
yourself. For example:
<blockquote>
<pre>
# Power zones for Sean Rhea
From BEGIN until 2006/07/17: # after original testing
1, Active Recovery, 122, 167
2, Endurance, 167, 228
3, Tempo, 228, 274
4, Lactate Threshold, 274, 319
5, VO2 Max, 319, 365
6, Anaerobic Capacity, 365, 678
7, Sprinting, 678, MAX
From 2006/07/17 until 2007/02/05: # since Workingman's ITT
1, Active Recovery, 135, 185
2, Endurance, 185, 253
3, Tempo, 253, 303
4, Lactate Threshold, 303, 354
5, VO2 Max, 354, 404
6, Anaerobic Capacity, 404, 752
7, Sprinting, 752, MAX
From 2007/02/05 until END: # since 20-min Diablo ITT
1, Active Recovery, 139, 191
2, Endurance, 191, 260
3, Tempo, 260, 312
4, Lactate Threshold, 312, 364
5, VO2 Max, 364, 416
6, Anaerobic Capacity, 416, 774
7, Sprinting, 774, MAX
</pre>
</blockquote>
If you copy the above into a file named "power.zones" in your
GoldenCheetah directory (e.g., "~/Library/GoldenCheetah/YourName"), GC
will display power zone information for all your rides using my power
zones.
<p>
The format should be pretty obvious. Comments start from a '#'
character and run until the end of a line.
<p>
A range goes from a date (in YYYY/MM/DD format) at which to start using
the following zones to a date before which to stop doing so. Also, you
can start the first range in a file with the keyword "BEGIN", which will
be treated as the earliest possible date, and you can end the last range
with the keyword "END", which will be treated as the latest. Also, in
order to have your zones displayed in the "Weekly Summary", each range
needs to start on a Monday.
<p>
After a range, you enter the zones to use during that range. Each zone
has a name (e.g., "2"), a description (e.g., "Endurance"), a low power
and a high power. The number of zones, their names and descriptions are
entirely up to you; I use ones similar to those advocated by Allen and
Coggan, as you might have noticed. The lower number in each range is
inclusive, and the upper one is not; i.e., you should read the range as
[low, high). Also, you can use the keywork "MAX" to indicate the
maximum possible power.

2
src/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Added this line to .gitattributes
*.pbxproj -crlf -diff -merge

27
src/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# xcode noise
build/*
*.pbxuser
*.mode1v3
Info.plist
*.xcodeproj
gcconfig.pri
# old skool
.svn
# osx noise
.DS_Store
profile
# ignore Qt moc files
moc_*
qrc_application.cpp
# ignore other object files
*.o
# ignore qmake-generated Makefile
Makefile
# ignore executables
GoldenCheetah*

658
src/AllPlot.cpp Normal file
View File

@@ -0,0 +1,658 @@
/*
* 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 "RideFile.h"
#include "RideItem.h"
#include "Settings.h"
#include "Zones.h"
#include <assert.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_marker.h>
#include <qwt_text.h>
#include <qwt_legend.h>
#include <qwt_data.h>
#include <QMultiMap>
// define a background class to handle shading of power zones
// draws power zone bands IF zones are defined and the option
// to draw bonds has been selected
class AllPlotBackground: public QwtPlotItem
{
private:
AllPlot *parent;
public:
AllPlotBackground(AllPlot *_parent)
{
setZ(0.0);
parent = _parent;
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
virtual void draw(QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &yMap,
const QRect &rect) const
{
RideItem *rideItem = parent->rideItem;
if (! rideItem)
return;
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
int num_zones = zone_lows.size();
if (num_zones > 0) {
for (int z = 0; z < num_zones; z ++) {
QRect r = rect;
QColor shading_color = zoneColor(z, num_zones);
shading_color.setHsv(
shading_color.hue(),
shading_color.saturation() / 4,
shading_color.value()
);
r.setBottom(yMap.transform(zone_lows[z]));
if (z + 1 < num_zones)
r.setTop(yMap.transform(zone_lows[z + 1]));
if (r.top() <= r.bottom())
painter->fillRect(r, shading_color);
}
}
}
}
};
// Zone labels are drawn if power zone bands are enabled, automatically
// at the center of the plot
class AllPlotZoneLabel: public QwtPlotItem
{
private:
AllPlot *parent;
int zone_number;
double watts;
QwtText text;
public:
AllPlotZoneLabel(AllPlot *_parent, int _zone_number)
{
parent = _parent;
zone_number = _zone_number;
RideItem *rideItem = parent->rideItem;
if (! rideItem)
return;
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
// create new zone labels if we're shading
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
QList <QString> zone_names = (*zones)->getZoneNames(zone_range);
int num_zones = zone_lows.size();
assert(zone_names.size() == num_zones);
if (zone_number < num_zones) {
watts =
(
(zone_number + 1 < num_zones) ?
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
(
(zone_number > 0) ?
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
2.0 * zone_lows[zone_number]
)
);
text = QwtText(zone_names[zone_number]);
text.setFont(QFont("Helvetica",24, QFont::Bold));
QColor text_color = zoneColor(zone_number, num_zones);
text_color.setAlpha(64);
text.setColor(text_color);
}
}
setZ(1.0 + zone_number / 100.0);
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
void draw(QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &yMap,
const QRect &rect) const
{
if (parent->shadeZones()) {
int x = (rect.left() + rect.right()) / 2;
int y = yMap.transform(watts);
// the following code based on source for QwtPlotMarker::draw()
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
tr.moveCenter(QPoint(x, y));
text.draw(painter, tr);
}
}
};
static inline double
max(double a, double b) { if (a > b) return a; else return b; }
#define MILES_PER_KM 0.62137119
#define FEET_PER_M 3.2808399
AllPlot::AllPlot():
settings(NULL),
unit(0),
d_mrk(NULL), rideItem(NULL),
hrArray(NULL), wattsArray(NULL),
speedArray(NULL), cadArray(NULL), timeArray(NULL),
distanceArray(NULL), altArray(NULL), interArray(NULL), smooth(30), bydist(false),
shade_zones(false)
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
unit = settings->value(GC_UNIT);
useMetricUnits = (unit.toString() == "Metric");
// create a background object for shading
bg = new AllPlotBackground(this);
bg->attach(this);
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setXTitle();
wattsCurve = new QwtPlotCurve("Power");
QPen wattsPen = QPen(Qt::red);
wattsPen.setWidth(2);
wattsCurve->setPen(wattsPen);
hrCurve = new QwtPlotCurve("Heart Rate");
QPen hrPen = QPen(Qt::blue);
hrPen.setWidth(2);
hrCurve->setPen(hrPen);
speedCurve = new QwtPlotCurve("Speed");
QPen speedPen = QPen(QColor(0, 204, 0));
speedPen.setWidth(2);
speedCurve->setPen(speedPen);
speedCurve->setYAxis(yRight);
cadCurve = new QwtPlotCurve("Cadence");
QPen cadPen = QPen(QColor(0, 204, 204));
cadPen.setWidth(2);
cadCurve->setPen(cadPen);
altCurve = new QwtPlotCurve("Altitude");
// altCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen *altPen = new QPen(QColor(124, 91, 31));
altPen->setWidth(1);
altCurve->setPen(*altPen);
QColor brush_color = QColor(124, 91, 31);
brush_color.setAlpha(64);
altCurve->setBrush(brush_color); // fill below the line
delete altPen;
grid = new QwtPlotGrid();
grid->enableX(false);
QPen gridPen;
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
grid->attach(this);
zoneLabels = QList <AllPlotZoneLabel *>::QList();
}
struct DataPoint {
double time, hr, watts, speed, cad, alt;
int inter;
DataPoint(double t, double h, double w, double s, double c, double a, int i) :
time(t), hr(h), watts(w), speed(s), cad(c), alt(a), inter(i) {}
};
bool AllPlot::shadeZones() const
{
return (shade_zones && wattsArray);
}
void AllPlot::refreshZoneLabels()
{
// delete any existing power zone labels
if (zoneLabels.size()) {
QListIterator<AllPlotZoneLabel *> i(zoneLabels);
while (i.hasNext()) {
AllPlotZoneLabel *label = i.next();
label->detach();
delete label;
}
}
zoneLabels.clear();
if (rideItem) {
int zone_range = rideItem->zoneRange();
Zones **zones = rideItem->zones;
// generate labels for existing zones
if (zones && *zones && (zone_range >= 0)) {
int num_zones = (*zones)->numZones(zone_range);
for (int z = 0; z < num_zones; z ++) {
AllPlotZoneLabel *label = new AllPlotZoneLabel(this, z);
label->attach(this);
zoneLabels.append(label);
}
}
}
}
void
AllPlot::recalc()
{
if (!timeArray)
return;
int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]);
if (rideTimeSecs > 7*24*60*60) {
QwtArray<double> data;
if (wattsArray)
wattsCurve->setData(data, data);
if (hrArray)
hrCurve->setData(data, data);
if (speedArray)
speedCurve->setData(data, data);
if (cadArray)
cadCurve->setData(data, data);
if (altArray)
altCurve->setData(data, data);
return;
}
double totalWatts = 0.0;
double totalHr = 0.0;
double totalSpeed = 0.0;
double totalCad = 0.0;
double totalDist = 0.0;
double totalAlt = 0.0;
QList<DataPoint*> list;
double *smoothWatts = new double[rideTimeSecs + 1];
double *smoothHr = new double[rideTimeSecs + 1];
double *smoothSpeed = new double[rideTimeSecs + 1];
double *smoothCad = new double[rideTimeSecs + 1];
double *smoothTime = new double[rideTimeSecs + 1];
double *smoothDistance = new double[rideTimeSecs + 1];
double *smoothAltitude = new double[rideTimeSecs + 1];
delete [] d_mrk;
QMap<double, int> interList; //Store the times and intervals
// Times are unique, intervals are not always
//Intervals are sequential on the PowerTap.
int lastInterval = 0; //Detect if we hit a new interval
for (int secs = 0; ((secs < smooth)
&& (secs < rideTimeSecs)); ++secs) {
smoothWatts[secs] = 0.0;
smoothHr[secs] = 0.0;
smoothSpeed[secs] = 0.0;
smoothCad[secs] = 0.0;
smoothTime[secs] = secs / 60.0;
smoothDistance[secs] = 0.0;
smoothAltitude[secs] = 0.0;
}
int i = 0;
for (int secs = smooth; secs <= rideTimeSecs; ++secs) {
while ((i < arrayLength) && (timeArray[i] <= secs)) {
DataPoint *dp =
new DataPoint(
timeArray[i],
(hrArray ? hrArray[i] : 0),
(wattsArray ? wattsArray[i] : 0),
(speedArray ? speedArray[i] : 0),
(cadArray ? cadArray[i] : 0),
(altArray ? altArray[i] : 0),
interArray[i]
);
if (wattsArray)
totalWatts += wattsArray[i];
if (hrArray)
totalHr += hrArray[i];
if (speedArray)
totalSpeed += speedArray[i];
if (cadArray)
totalCad += cadArray[i];
if (altArray)
totalAlt += altArray[i];
totalDist = distanceArray[i];
list.append(dp);
//Figure out when and if we have a new interval..
if(lastInterval != interArray[i]) {
lastInterval = interArray[i];
interList[secs/60.0] = lastInterval;
}
++i;
}
while (!list.empty() && (list.front()->time < secs - smooth)) {
DataPoint *dp = list.front();
list.removeFirst();
totalWatts -= dp->watts;
totalHr -= dp->hr;
totalSpeed -= dp->speed;
totalCad -= dp->cad;
totalAlt -= dp->alt;
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;
smoothSpeed[secs] = 0.0;
smoothCad[secs] = 0.0;
smoothAltitude[secs] = smoothAltitude[secs - 1];
}
else {
smoothWatts[secs] = totalWatts / list.size();
smoothHr[secs] = totalHr / list.size();
smoothSpeed[secs] = totalSpeed / list.size();
smoothCad[secs] = totalCad / list.size();
smoothAltitude[secs] = totalAlt / list.size();
}
smoothDistance[secs] = totalDist;
smoothTime[secs] = secs / 60.0;
}
double *xaxis = bydist ? smoothDistance : smoothTime;
// set curves
if (wattsArray)
wattsCurve->setData(xaxis, smoothWatts, rideTimeSecs + 1);
if (hrArray)
hrCurve->setData(xaxis, smoothHr, rideTimeSecs + 1);
if (speedArray)
speedCurve->setData(xaxis, smoothSpeed, rideTimeSecs + 1);
if (cadArray)
cadCurve->setData(xaxis, smoothCad, rideTimeSecs + 1);
if (altArray)
altCurve->setData(xaxis, smoothAltitude, rideTimeSecs + 1);
setAxisScale(xBottom, 0.0, bydist ? totalDist : smoothTime[rideTimeSecs]);
setYMax();
refreshZoneLabels();
//QList<double> interTimes = interList.keys();
QString label[interList.count()];
QwtText text[interList.count()];
d_mrk = new QwtPlotMarker[interList.count()];
int x = 0;
double time;
foreach(time, interList.keys()) {
// marker
d_mrk[x].setValue(0,0);
d_mrk[x].setLineStyle(QwtPlotMarker::VLine);
d_mrk[x].setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
d_mrk[x].setLinePen(QPen(Qt::black, 0, Qt::DashDotLine));
d_mrk[x].attach(this);
label[x] = QString::number(interList[time]);
text[x] = QwtText(label[x]);
text[x].setFont(QFont("Helvetica", 10, QFont::Bold));
text[x].setColor(Qt::black);
if (!bydist)
d_mrk[x].setValue(time, 0.0);
else
d_mrk[x].setValue(smoothDistance[int(ceil(60*time))], 0.0);
d_mrk[x].setLabel(text[x]);
x++;
}
replot();
if(smoothWatts != NULL)
delete [] smoothWatts;
if(smoothHr != NULL)
delete [] smoothHr;
if(smoothSpeed != NULL)
delete [] smoothSpeed;
if(smoothCad != NULL)
delete [] smoothCad;
if(smoothTime != NULL)
delete [] smoothTime;
if(smoothDistance != NULL)
delete [] smoothDistance;
if(smoothAltitude != NULL)
delete [] smoothAltitude;
}
void
AllPlot::setYMax()
{
double ymax = 0;
QString ylabel = "";
if (wattsCurve->isVisible()) {
ymax = max(ymax, wattsCurve->maxYValue());
ylabel += QString((ylabel == "") ? "" : " / ") + "Watts";
}
if (hrCurve->isVisible()) {
ymax = max(ymax, hrCurve->maxYValue());
ylabel += QString((ylabel == "") ? "" : " / ") + "BPM";
}
if (cadCurve->isVisible()) {
ymax = max(ymax, cadCurve->maxYValue());
ylabel += QString((ylabel == "") ? "" : " / ") + "RPM";
}
if (altCurve->isVisible()) {
ymax = max(ymax, altCurve->maxYValue());
if (useMetricUnits){
ylabel += QString((ylabel == "") ? "" : " / ") + "Meters";
} else {
ylabel += QString((ylabel == "") ? "" : " / ") + "Ft";
}
}
setAxisScale(yLeft, 0.0, ymax * 1.1);
setAxisTitle(yLeft, ylabel);
enableAxis(yRight, speedCurve->isVisible());
setAxisTitle(yRight, (useMetricUnits ? "KPH" : "MPH"));
}
void
AllPlot::setXTitle()
{
if (bydist)
setAxisTitle(xBottom, "Distance "+QString(unit.toString() == "Metric"?"(km)":"(miles)"));
else
setAxisTitle(xBottom, "Time (minutes)");
}
void
AllPlot::setData(RideItem *_rideItem)
{
rideItem = _rideItem;
if(wattsArray != NULL)
delete [] wattsArray;
if(hrArray != NULL)
delete [] hrArray;
if(speedArray != NULL)
delete [] speedArray;
if(cadArray != NULL)
delete [] cadArray;
if(timeArray != NULL)
delete [] timeArray;
if(interArray != NULL)
delete [] interArray;
if(distanceArray != NULL)
delete [] distanceArray;
if(altArray != NULL)
delete [] altArray;
RideFile *ride = rideItem->ride;
if (ride) {
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
RideFileDataPresent *dataPresent = ride->areDataPresent();
int npoints = ride->dataPoints().size();
wattsArray = dataPresent->watts ? new double[npoints] : NULL;
hrArray = dataPresent->hr ? new double[npoints] : NULL;
speedArray = dataPresent->kph ? new double[npoints] : NULL;
cadArray = dataPresent->cad ? new double[npoints] : NULL;
altArray = dataPresent->alt ? new double[npoints] : NULL;
timeArray = new double[npoints];
interArray = new int[npoints];
distanceArray = new double[npoints];
// attach appropriate curves
wattsCurve->detach();
hrCurve->detach();
speedCurve->detach();
cadCurve->detach();
altCurve->detach();
if (wattsArray) wattsCurve->attach(this);
if (hrArray) hrCurve->attach(this);
if (speedArray) speedCurve->attach(this);
if (cadArray) cadCurve->attach(this);
if (altArray) altCurve->attach(this);
arrayLength = 0;
QListIterator<RideFilePoint*> i(ride->dataPoints());
while (i.hasNext()) {
RideFilePoint *point = i.next();
timeArray[arrayLength] = point->secs;
if (wattsArray)
wattsArray[arrayLength] = max(0, point->watts);
if (hrArray)
hrArray[arrayLength] = max(0, point->hr);
if (speedArray)
speedArray[arrayLength] = max(0,
(useMetricUnits
? point->kph
: point->kph * MILES_PER_KM));
if (cadArray)
cadArray[arrayLength] = max(0, point->cad);
if (altArray)
altArray[arrayLength] = max(0,
(useMetricUnits
? point->alt
: point->alt * FEET_PER_M));
interArray[arrayLength] = point->interval;
distanceArray[arrayLength] = max(0,
(useMetricUnits
? point->km
: point->km * MILES_PER_KM));
++arrayLength;
}
recalc();
}
else {
setTitle("no data");
wattsCurve->detach();
hrCurve->detach();
speedCurve->detach();
cadCurve->detach();
altCurve->detach();
}
}
void
AllPlot::showPower(int id)
{
wattsCurve->setVisible(id < 2);
shade_zones = (id == 0);
setYMax();
recalc();
}
void
AllPlot::showHr(int state)
{
assert(state != Qt::PartiallyChecked);
hrCurve->setVisible(state == Qt::Checked);
setYMax();
replot();
}
void
AllPlot::showSpeed(int state)
{
assert(state != Qt::PartiallyChecked);
speedCurve->setVisible(state == Qt::Checked);
setYMax();
replot();
}
void
AllPlot::showCad(int state)
{
assert(state != Qt::PartiallyChecked);
cadCurve->setVisible(state == Qt::Checked);
setYMax();
replot();
}
void
AllPlot::showAlt(int state)
{
assert(state != Qt::PartiallyChecked);
altCurve->setVisible(state == Qt::Checked);
setYMax();
replot();
}
void
AllPlot::showGrid(int state)
{
assert(state != Qt::PartiallyChecked);
grid->setVisible(state == Qt::Checked);
replot();
}
void
AllPlot::setSmoothing(int value)
{
smooth = value;
recalc();
}
void
AllPlot::setByDistance(int id)
{
bydist = (id == 1);
setXTitle();
recalc();
}

View File

@@ -1,6 +1,4 @@
/*
* $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
@@ -22,27 +20,51 @@
#define _GC_AllPlot_h 1
#include <qwt_plot.h>
#include <QtGui>
#include <qpainter.h>
class QPen;
class QwtPlotCurve;
class QwtPlotGrid;
class RawFile;
class QwtPlotMarker;
class RideItem;
class RideFile;
class AllPlot;
class AllPlotBackground;
class AllPlotZoneLabel;
class AllPlot : public QwtPlot
{
Q_OBJECT
private:
AllPlotBackground *bg;
QSettings *settings;
QVariant unit;
public:
QwtPlotCurve *wattsCurve;
QwtPlotCurve *hrCurve;
QwtPlotCurve *speedCurve;
QwtPlotCurve *cadCurve;
QwtPlotCurve *altCurve;
QwtPlotMarker *d_mrk;
QList <AllPlotZoneLabel *> zoneLabels;
AllPlot();
int smoothing() const { return smooth; }
void setData(RawFile *raw);
bool byDistance() const { return bydist; }
bool shadeZones() const;
void refreshZoneLabels();
void setData(RideItem *_rideItem);
RideItem *rideItem;
public slots:
@@ -50,8 +72,10 @@ class AllPlot : public QwtPlot
void showHr(int state);
void showSpeed(int state);
void showCad(int state);
void showAlt(int state);
void showGrid(int state);
void setSmoothing(int value);
void setByDistance(int value);
protected:
@@ -62,12 +86,21 @@ class AllPlot : public QwtPlot
double *speedArray;
double *cadArray;
double *timeArray;
double *distanceArray;
double *altArray;
int arrayLength;
int *interArray;
int smooth;
bool bydist;
void recalc();
void setYMax();
void setXTitle();
bool shade_zones; // whether power should be shaded
bool useMetricUnits; // whether metric units are used (or imperial)
};
#endif // _GC_AllPlot_h

256
src/BasicRideMetrics.cpp Normal file
View File

@@ -0,0 +1,256 @@
/*
* Copyright (c) 2008 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 "RideMetric.h"
#define MILES_PER_KM 0.62137119
#define FEET_PER_METER 3.2808399
class WorkoutTime : public RideMetric {
double seconds;
public:
WorkoutTime() : seconds(0.0) {}
QString name() const { return "workout_time"; }
QString units(bool) const { return "seconds"; }
double value(bool) const { return seconds; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
seconds = ride->dataPoints().back()->secs;
}
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) { seconds += other->value(true); }
RideMetric *clone() const { return new WorkoutTime(*this); }
};
static bool workoutTimeAdded =
RideMetricFactory::instance().addMetric(WorkoutTime());
//////////////////////////////////////////////////////////////////////////////
class TimeRiding : public PointwiseRideMetric {
double secsMovingOrPedaling;
public:
TimeRiding() : secsMovingOrPedaling(0.0) {}
QString name() const { return "time_riding"; }
QString units(bool) const { return "seconds"; }
double value(bool) const { return secsMovingOrPedaling; }
void perPoint(const RideFilePoint *point, double secsDelta,
const RideFile *, const Zones *, int) {
if ((point->kph > 0.0) || (point->cad > 0.0))
secsMovingOrPedaling += secsDelta;
}
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) {
secsMovingOrPedaling += other->value(true);
}
RideMetric *clone() const { return new TimeRiding(*this); }
};
static bool timeRidingAdded =
RideMetricFactory::instance().addMetric(TimeRiding());
//////////////////////////////////////////////////////////////////////////////
class TotalDistance : public RideMetric {
double km;
public:
TotalDistance() : km(0.0) {}
QString name() const { return "total_distance"; }
QString units(bool metric) const { return metric ? "km" : "miles"; }
double value(bool metric) const {
return metric ? km : (km * MILES_PER_KM);
}
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
km = ride->dataPoints().back()->km;
}
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) { km += other->value(true); }
RideMetric *clone() const { return new TotalDistance(*this); }
};
static bool totalDistanceAdded =
RideMetricFactory::instance().addMetric(TotalDistance());
//////////////////////////////////////////////////////////////////////////////
class ElevationGain : public PointwiseRideMetric {
double elegain;
double prevalt;
public:
ElevationGain() : elegain(0.0), prevalt(0.0) {}
QString name() const { return "elevation_gain"; }
QString units(bool metric) const { return metric ? "meters" : "feet"; }
double value(bool metric) const {
return metric ? elegain : (elegain * FEET_PER_METER);
}
void perPoint(const RideFilePoint *point, double,
const RideFile *, const Zones *, int) {
if (prevalt <= 0){
prevalt = point->alt;
} else if (prevalt <= point->alt) {
elegain += (point->alt-prevalt);
prevalt = point->alt;
} else {
prevalt = point->alt;
}
}
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) {
elegain += other->value(true);
}
RideMetric *clone() const { return new ElevationGain(*this); }
};
static bool elevationGainAdded =
RideMetricFactory::instance().addMetric(ElevationGain());
//////////////////////////////////////////////////////////////////////////////
class TotalWork : public PointwiseRideMetric {
double joules;
public:
TotalWork() : joules(0.0) {}
QString name() const { return "total_work"; }
QString units(bool) const { return "kJ"; }
double value(bool) const { return joules / 1000.0; }
void perPoint(const RideFilePoint *point, double secsDelta,
const RideFile *, const Zones *, int) {
if (point->watts >= 0.0)
joules += point->watts * secsDelta;
}
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) {
assert(name() == other->name());
TotalWork *tw = dynamic_cast<TotalWork*>(other);
joules += tw->joules;
}
RideMetric *clone() const { return new TotalWork(*this); }
};
static bool totalWorkAdded =
RideMetricFactory::instance().addMetric(TotalWork());
//////////////////////////////////////////////////////////////////////////////
class AvgSpeed : public PointwiseRideMetric {
double secsMoving;
double km;
public:
AvgSpeed() : secsMoving(0.0), km(0.0) {}
QString name() const { return "average_speed"; }
QString units(bool metric) const { return metric ? "kph" : "mph"; }
double value(bool metric) const {
if (secsMoving == 0.0) return 0.0;
double kph = km / secsMoving * 3600.0;
return metric ? kph : (kph * MILES_PER_KM);
}
void compute(const RideFile *ride, const Zones *zones, int zoneRange,
const QHash<QString,RideMetric*> &deps) {
PointwiseRideMetric::compute(ride, zones, zoneRange, deps);
km = ride->dataPoints().back()->km;
}
void perPoint(const RideFilePoint *point, double secsDelta,
const RideFile *, const Zones *, int) {
if (point->kph > 0.0) secsMoving += secsDelta;
}
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) {
assert(name() == other->name());
AvgSpeed *as = dynamic_cast<AvgSpeed*>(other);
secsMoving += as->secsMoving;
km += as->km;
}
RideMetric *clone() const { return new AvgSpeed(*this); }
};
static bool avgSpeedAdded =
RideMetricFactory::instance().addMetric(AvgSpeed());
//////////////////////////////////////////////////////////////////////////////
struct AvgPower : public AvgRideMetric {
QString name() const { return "average_power"; }
QString units(bool) const { return "watts"; }
void perPoint(const RideFilePoint *point, double,
const RideFile *, const Zones *, int) {
if (point->watts >= 0.0) {
total += point->watts;
++count;
}
}
RideMetric *clone() const { return new AvgPower(*this); }
};
static bool avgPowerAdded =
RideMetricFactory::instance().addMetric(AvgPower());
//////////////////////////////////////////////////////////////////////////////
struct AvgHeartRate : public AvgRideMetric {
QString name() const { return "average_hr"; }
QString units(bool) const { return "bpm"; }
void perPoint(const RideFilePoint *point, double,
const RideFile *, const Zones *, int) {
if (point->hr > 0) {
total += point->hr;
++count;
}
}
RideMetric *clone() const { return new AvgHeartRate(*this); }
};
static bool avgHeartRateAdded =
RideMetricFactory::instance().addMetric(AvgHeartRate());
//////////////////////////////////////////////////////////////////////////////
struct AvgCadence : public AvgRideMetric {
QString name() const { return "average_cad"; }
QString units(bool) const { return "rpm"; }
void perPoint(const RideFilePoint *point, double,
const RideFile *, const Zones *, int) {
if (point->cad > 0) {
total += point->cad;
++count;
}
}
RideMetric *clone() const { return new AvgCadence(*this); }
};
static bool avgCadenceAdded =
RideMetricFactory::instance().addMetric(AvgCadence());

179
src/BestIntervalDialog.cpp Normal file
View File

@@ -0,0 +1,179 @@
/*
* Copyright (c) 2008 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 "BestIntervalDialog.h"
#include "MainWindow.h"
#include "RideFile.h"
#include <QMap>
#include <assert.h>
#include <math.h>
BestIntervalDialog::BestIntervalDialog(MainWindow *mainWindow) :
mainWindow(mainWindow)
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle("Find Best Intervals");
QVBoxLayout *mainLayout = new QVBoxLayout(this);
QHBoxLayout *intervalLengthLayout = new QHBoxLayout;
QLabel *intervalLengthLabel = new QLabel(tr("Interval length: "), this);
intervalLengthLayout->addWidget(intervalLengthLabel);
hrsSpinBox = new QDoubleSpinBox(this);
hrsSpinBox->setDecimals(0);
hrsSpinBox->setMinimum(0.0);
hrsSpinBox->setSuffix(" hrs");
hrsSpinBox->setSingleStep(1.0);
hrsSpinBox->setAlignment(Qt::AlignRight);
intervalLengthLayout->addWidget(hrsSpinBox);
minsSpinBox = new QDoubleSpinBox(this);
minsSpinBox->setDecimals(0);
minsSpinBox->setRange(0.0, 59.0);
minsSpinBox->setSuffix(" mins");
minsSpinBox->setSingleStep(1.0);
minsSpinBox->setWrapping(true);
minsSpinBox->setAlignment(Qt::AlignRight);
minsSpinBox->setValue(1.0);
intervalLengthLayout->addWidget(minsSpinBox);
secsSpinBox = new QDoubleSpinBox(this);
secsSpinBox->setDecimals(0);
minsSpinBox->setRange(0.0, 59.0);
secsSpinBox->setSuffix(" secs");
secsSpinBox->setSingleStep(1.0);
secsSpinBox->setWrapping(true);
secsSpinBox->setAlignment(Qt::AlignRight);
intervalLengthLayout->addWidget(secsSpinBox);
mainLayout->addLayout(intervalLengthLayout);
QHBoxLayout *intervalCountLayout = new QHBoxLayout;
QLabel *intervalCountLabel = new QLabel(tr("How many to find: "), this);
intervalCountLayout->addWidget(intervalCountLabel);
countSpinBox = new QDoubleSpinBox(this);
countSpinBox->setDecimals(0);
countSpinBox->setMinimum(1.0);
countSpinBox->setSingleStep(1.0);
countSpinBox->setAlignment(Qt::AlignRight);
intervalCountLayout->addWidget(countSpinBox);
mainLayout->addLayout(intervalCountLayout);
QLabel *resultsLabel = new QLabel(tr("Results:"), this);
mainLayout->addWidget(resultsLabel);
resultsText = new QTextEdit(this);
resultsText->setReadOnly(true);
mainLayout->addWidget(resultsText);
QHBoxLayout *buttonLayout = new QHBoxLayout;
findButton = new QPushButton(tr("&Find Intervals"), this);
buttonLayout->addWidget(findButton);
doneButton = new QPushButton(tr("&Done"), this);
buttonLayout->addWidget(doneButton);
mainLayout->addLayout(buttonLayout);
connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked()));
connect(doneButton, SIGNAL(clicked()), this, SLOT(doneClicked()));
}
void
BestIntervalDialog::findClicked()
{
const RideFile *ride = mainWindow->currentRide();
if (!ride) {
QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!"));
return;
}
int maxIntervals = (int) countSpinBox->value();
double windowSizeSecs = (hrsSpinBox->value() * 3600.0
+ minsSpinBox->value() * 60.0
+ secsSpinBox->value());
if (windowSizeSecs == 0.0) {
QMessageBox::critical(this, tr("Bad Interval Length"),
tr("Interval length must be greater than zero!"));
return;
}
QList<const RideFilePoint*> window;
QMap<double,double> bests;
double secsDelta = ride->recIntSecs();
int expectedSamples = (int) floor(windowSizeSecs / secsDelta);
double totalWatts = 0.0;
QListIterator<RideFilePoint*> i(ride->dataPoints());
while (i.hasNext()) {
const RideFilePoint *point = i.next();
while (!window.empty()
&& (point->secs >= window.first()->secs + windowSizeSecs)) {
totalWatts -= window.first()->watts;
window.takeFirst();
}
totalWatts += point->watts;
window.append(point);
int divisor = std::max(window.size(), expectedSamples);
double avg = totalWatts / divisor;
bests.insertMulti(avg, point->secs);
}
QMap<double,double> results;
while (!bests.empty() && maxIntervals--) {
QMutableMapIterator<double,double> j(bests);
j.toBack();
j.previous();
double secs = j.value();
results.insert(j.value() - windowSizeSecs, j.key());
j.remove();
while (j.hasPrevious()) {
j.previous();
if (abs(secs - j.value()) < windowSizeSecs)
j.remove();
}
}
QString resultsHtml =
"<center>"
"<table width=\"80%\">"
"<tr><td align=\"center\">Start Time</td>"
" <td align=\"center\">Average Power</td></tr>";
QMapIterator<double,double> j(results);
while (j.hasNext()) {
j.next();
double secs = j.key();
double mins = ((int) secs) / 60;
secs = secs - mins * 60.0;
double hrs = ((int) mins) / 60;
mins = mins - hrs * 60.0;
QString row =
"<tr><td align=\"center\">%1:%2:%3</td>"
" <td align=\"center\">%4</td></tr>";
row = row.arg(hrs, 0, 'f', 0);
row = row.arg(mins, 2, 'f', 0, QLatin1Char('0'));
row = row.arg(secs, 2, 'f', 0, QLatin1Char('0'));
row = row.arg(j.value(), 0, 'f', 0, QLatin1Char('0'));
resultsHtml += row;
}
resultsHtml += "</table></center>";
resultsText->setHtml(resultsHtml);
}
void
BestIntervalDialog::doneClicked()
{
done(0);
}

View File

@@ -1,7 +1,5 @@
/*
* $Id: PowerHist.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
*
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
* Copyright (c) 2008 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
@@ -18,45 +16,31 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GC_PowerHist_h
#define _GC_PowerHist_h 1
#ifndef _GC_BestIntervalDialog_h
#define _GC_BestIntervalDialog_h 1
#include <qwt_plot.h>
#include <QtGui>
class QwtPlotCurve;
class QwtPlotGrid;
class RawFile;
class MainWindow;
class PowerHist : public QwtPlot
class BestIntervalDialog : public QDialog
{
Q_OBJECT
public:
BestIntervalDialog(MainWindow *mainWindow);
QwtPlotCurve *curve;
private slots:
void findClicked();
void doneClicked();
PowerHist();
int binWidth() const { return binw; }
void setData(RawFile *raw);
public slots:
void setBinWidth(int value);
protected:
QwtPlotGrid *grid;
double *array;
int arrayLength;
int binw;
void recalc();
void setYMax();
private:
MainWindow *mainWindow;
QPushButton *findButton, *doneButton;
QDoubleSpinBox *hrsSpinBox, *minsSpinBox, *secsSpinBox, *countSpinBox;
QTextEdit *resultsText;
};
#endif // _GC_PowerHist_h
#endif // _GC_BestIntervalDialog_h

240
src/BikeScore.cpp Normal file
View File

@@ -0,0 +1,240 @@
/*
* Copyright (c) 2008 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 "RideMetric.h"
#include "Zones.h"
#include <math.h>
const double bikeScoreN = 4.0;
const double bikeScoreTau = 25.0;
// NOTE: This code follows the description of xPower, Relative Intensity, and
// BikeScore in "Analysis of Power Output and Training Stress in Cyclists: The
// Development of the BikeScore(TM) Algorithm", page 5, by Phil Skiba:
//
// http://www.physfarm.com/Analysis%20of%20Power%20Output%20and%20Training%20Stress%20in%20Cyclists-%20BikeScore.pdf
//
// The weighting factors for the exponentially weighted average are taken from
// a spreadsheet provided by Dr. Skiba.
class XPower : public RideMetric {
double xpower;
double secs;
friend class RelativeIntensity;
friend class BikeScore;
public:
XPower() : xpower(0.0), secs(0.0) {}
QString name() const { return "skiba_xpower"; }
QString units(bool) const { return "watts"; }
double value(bool) const { return xpower; }
void compute(const RideFile *ride, const Zones *, int,
const QHash<QString,RideMetric*> &) {
double secsDelta = ride->recIntSecs();
// djconnel:
double attenuation = exp(-secsDelta / bikeScoreTau);
double sampleWeight = 1 - attenuation;
double lastSecs = 0.0; // previous point
double initialSecs = 0.0; // time associated with start of data
double weighted = 0.0; // exponentially smoothed power
double epsilon_time = secsDelta / 100; // for comparison of times
double total = 0.0;
/* djconnel: calculate bikescore:
For all values of t, smoothed power
p* = integral { -infinity to t } (1/tau) exp[(t' - t) / tau] p(t') dt'
From this we calculate an integral, xw:
xw = integral {t0 to t} { p*^N dt }
(in the code, p* -> "weighted"; xw -> "total")
During any interval t0 <= t < t1, with p* = p*(t0) at the start of
the interval, with power p constant during the interval:
p*(t) = p*(t0) exp[(t0 - t) / tau] + p ( 1 - exp[(t0 - t) / tau] )
So the contribution to xw is then:
delta_xw = integral { t0 to t1 } [ p*(t0) exp[(t0 - t) / tau] + p ( 1 - exp[(t0 - t) / tau] ) ]^N
Consider the simplified case p = 0, and t1 = t0 + deltat, then this is evaluated:
delta_xw = integral { t0 to t1 } ( p*(t0) exp[(t0 - t) / tau] )^N
= integral { t0 to t1 } ( p*(t0)^N exp[N (t0 - t) / tau] )
= (tau / N) p*(t0)^N (1 - exp[-N deltat / tau])
This is the component which should be added to xw during idle periods.
More generally:
delta_xw = integral { t0 to t1 } [ p*(t0) exp[(t0 - t) / tau] + p ( 1 - exp[(t0 - t) / tau] ) ]^N
= integral { 0 to deltat }
[
p*(t0)^N exp[-N t' / tau] +
N p*(t0)^(N - 1) p exp[-(N - 1) t' / tau] (1 - exp[-t' / tau]) +
[N (N - 1) / 2] p*(t0)^(N - 2) p^2 exp[-(N - 2) t' / tau] (1 - exp[-2 t' / tau]) +
[N (N - 1) (N - 2) / 6] p*(t0)^(N - 3) p^3 exp[-(N - 3) t' / tau] (1 - exp[-3 t' / tau]) +
[N (N - 1) (N - 2) (N - 3) / 24] p*(t0)^(N - 4) p^4 exp[-(N - 4) t' / tau] (1 - exp[-4 t' / tau]) +
...
] dt'
but a linearized solution is fine as long as the sampling interval is << the smoothing time.
*/
QListIterator<RideFilePoint*> i(ride->dataPoints());
int count = 0;
while (i.hasNext()) {
const RideFilePoint *point = i.next();
// if there are missing data then add in the contribution
// from the exponentially decaying smoothed power
if (count == 0)
initialSecs = point->secs - secsDelta;
else {
double dt = point->secs - lastSecs - secsDelta;
if (dt > epsilon_time) {
double alpha = exp(-bikeScoreN * dt / bikeScoreTau);
total +=
(bikeScoreTau / bikeScoreN) * pow(weighted, bikeScoreN) * (1 - alpha);
weighted *= exp(-dt / bikeScoreTau);
}
}
// the existing weighted average is exponentially decayed by one sampling time,
// then the contribution from the present point is added
weighted = attenuation * weighted + sampleWeight * point->watts;
total += pow(weighted, bikeScoreN);
lastSecs = point->secs;
count++;
}
// after the ride is over, assume idleness (exponentially decaying smoothed power) to infinity
total +=
(bikeScoreTau / bikeScoreN) * pow(weighted, bikeScoreN);
secs = lastSecs - initialSecs;
xpower = (secs > 0) ?
pow(total * secsDelta / secs, 1 / bikeScoreN) :
0.0;
}
// added djconnel: allow RI to be combined across rides
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) {
assert(name() == other->name());
XPower *ap = dynamic_cast<XPower*>(other);
xpower = pow(xpower, bikeScoreN) * secs + pow(ap->xpower, bikeScoreN) * ap->secs;
secs += ap->secs;
xpower = pow(xpower / secs, 1 / bikeScoreN);
}
// end added djconnel
RideMetric *clone() const { return new XPower(*this); }
};
class RelativeIntensity : public RideMetric {
double reli;
double secs;
public:
RelativeIntensity() : reli(0.0), secs(0.0) {}
QString name() const { return "skiba_relative_intensity"; }
QString units(bool) const { return ""; }
double value(bool) const { return reli; }
void compute(const RideFile *, const Zones *zones, int zoneRange,
const QHash<QString,RideMetric*> &deps) {
if (zones) {
assert(deps.contains("skiba_xpower"));
XPower *xp = dynamic_cast<XPower*>(deps.value("skiba_xpower"));
assert(xp);
reli = xp->xpower / zones->getCP(zoneRange);
secs = xp->secs;
}
}
// added djconnel: allow RI to be combined across rides
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) {
assert(name() == other->name());
RelativeIntensity *ap = dynamic_cast<RelativeIntensity*>(other);
reli = secs * pow(reli, bikeScoreN) + ap->secs * pow(ap->reli, bikeScoreN);
secs += ap->secs;
reli = pow(reli / secs, 1.0 / bikeScoreN);
}
// end added djconnel
RideMetric *clone() const { return new RelativeIntensity(*this); }
};
class BikeScore : public RideMetric {
double score;
public:
BikeScore() : score(0.0) {}
QString name() const { return "skiba_bike_score"; }
QString units(bool) const { return ""; }
double value(bool) const { return score; }
void compute(const RideFile *ride, const Zones *zones, int zoneRange,
const QHash<QString,RideMetric*> &deps) {
if (!zones)
return;
if (ride->deviceType() == QString("Manual CSV")) {
// manual entry, use BS from dataPoints
QListIterator<RideFilePoint*> i(ride->dataPoints());
const RideFilePoint *point = i.next();
score = point->bs;
}
else {
assert(deps.contains("skiba_xpower"));
assert(deps.contains("skiba_relative_intensity"));
XPower *xp = dynamic_cast<XPower*>(deps.value("skiba_xpower"));
RideMetric *ri = deps.value("skiba_relative_intensity");
assert(ri);
double normWork = xp->xpower * xp->secs;
double rawBikeScore = normWork * ri->value(true);
double workInAnHourAtCP = zones->getCP(zoneRange) * 3600;
score = rawBikeScore / workInAnHourAtCP * 100.0;
}
}
RideMetric *clone() const { return new BikeScore(*this); }
bool canAggregate() const { return true; }
void aggregateWith(RideMetric *other) { score += other->value(true); }
};
static bool addAllThree() {
RideMetricFactory::instance().addMetric(XPower());
QVector<QString> deps;
deps.append("skiba_xpower");
RideMetricFactory::instance().addMetric(RelativeIntensity(), &deps);
deps.append("skiba_relative_intensity");
RideMetricFactory::instance().addMetric(BikeScore(), &deps);
return true;
}
static bool allThreeAdded = addAllThree();

View File

@@ -1,6 +1,4 @@
/*
* $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

View File

@@ -1,6 +1,4 @@
/*
* $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

47
src/CommPort.cpp Normal file
View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2008 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 "CommPort.h"
#include "Serial.h"
static QVector<CommPort::ListFunction> *listFunctions;
bool
CommPort::addListFunction(ListFunction f)
{
if (!listFunctions)
listFunctions = new QVector<CommPort::ListFunction>;
listFunctions->append(f);
return true;
}
QVector<CommPortPtr>
CommPort::listCommPorts(QString &err)
{
err = "";
QVector<CommPortPtr> result;
for (int i = 0; listFunctions && i < listFunctions->size(); ++i) {
QVector<CommPortPtr> tmp = (*listFunctions)[i](err);
if (err == "")
result << tmp;
else
err += "\n";
}
return result;
}

46
src/CommPort.h Normal file
View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2008 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_CommPort_h
#define _GC_CommPort_h 1
#include <QtCore>
#include <boost/shared_ptr.hpp>
class CommPort;
typedef boost::shared_ptr<CommPort> CommPortPtr;
class CommPort
{
public:
typedef QVector<CommPortPtr> (*ListFunction)(QString &err);
static bool addListFunction(ListFunction f);
static QVector<CommPortPtr> listCommPorts(QString &err);
virtual ~CommPort() {}
virtual bool open(QString &err) = 0;
virtual void close() = 0;
virtual int read(void *buf, size_t nbyte, QString &err) = 0;
virtual int write(void *buf, size_t nbyte, QString &err) = 0;
virtual QString name() const = 0;
};
#endif // _GC_CommPort_h

240
src/ConfigDialog.cpp Normal file
View File

@@ -0,0 +1,240 @@
#include <QtGui>
#include <QSettings>
#include <assert.h>
#include "MainWindow.h"
#include "ConfigDialog.h"
#include "Pages.h"
#include "Settings.h"
#include "Zones.h"
/* cyclist dialog protocol redesign:
* no zones:
* calendar disabled
* automatically go into "new" mode
* zone(s) defined:
* click on calendar: sets current zone to that associated with date
* save clicked:
* if new mode, create a new zone starting at selected date, or for all dates
* if this is only zone.
* delete clicked:
* deletes currently selected zone
*/
ConfigDialog::ConfigDialog(QDir _home, Zones **_zones)
{
home = _home;
zones = _zones;
assert(zones);
cyclistPage = new CyclistPage(zones);
contentsWidget = new QListWidget;
contentsWidget->setViewMode(QListView::IconMode);
contentsWidget->setIconSize(QSize(96, 84));
contentsWidget->setMovement(QListView::Static);
contentsWidget->setMinimumWidth(128);
contentsWidget->setMaximumWidth(128);
contentsWidget->setMinimumHeight(128);
contentsWidget->setSpacing(12);
configPage = new ConfigurationPage();
pagesWidget = new QStackedWidget;
pagesWidget->addWidget(configPage);
pagesWidget->addWidget(cyclistPage);
closeButton = new QPushButton(tr("Close"));
saveButton = new QPushButton(tr("Save"));
createIcons();
contentsWidget->setCurrentItem(contentsWidget->item(0));
// connect(closeButton, SIGNAL(clicked()), this, SLOT(reject()));
// connect(saveButton, SIGNAL(clicked()), this, SLOT(accept()));
connect(closeButton, SIGNAL(clicked()), this, SLOT(accept()));
connect(cyclistPage->btnBack, SIGNAL(clicked()), this, SLOT(back_Clicked()));
connect(cyclistPage->btnForward, SIGNAL(clicked()), this, SLOT(forward_Clicked()));
connect(cyclistPage->btnDelete, SIGNAL(clicked()), this, SLOT(delete_Clicked()));
connect(cyclistPage->calendar, SIGNAL(selectionChanged()), this, SLOT(calendarDateChanged()));
horizontalLayout = new QHBoxLayout;
horizontalLayout->addWidget(contentsWidget);
horizontalLayout->addWidget(pagesWidget, 1);
buttonsLayout = new QHBoxLayout;
buttonsLayout->addStretch(1);
buttonsLayout->addWidget(closeButton);
buttonsLayout->addWidget(saveButton);
mainLayout = new QVBoxLayout;
mainLayout->addLayout(horizontalLayout);
mainLayout->addStretch(1);
mainLayout->addSpacing(12);
mainLayout->addLayout(buttonsLayout);
setLayout(mainLayout);
setWindowTitle(tr("Config Dialog"));
}
ConfigDialog::~ConfigDialog()
{
delete cyclistPage;
delete contentsWidget;
delete configPage;
delete pagesWidget;
delete closeButton;
delete horizontalLayout;
delete buttonsLayout;
delete mainLayout;
}
void ConfigDialog::createIcons()
{
QListWidgetItem *configButton = new QListWidgetItem(contentsWidget);
configButton->setIcon(QIcon(":/images/config.png"));
configButton->setText(tr("Configuration"));
configButton->setTextAlignment(Qt::AlignHCenter);
configButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
QListWidgetItem *cyclistButton = new QListWidgetItem(contentsWidget);
cyclistButton->setIcon(QIcon(":images/cyclist.png"));
cyclistButton->setText(tr("Cyclist Info"));
cyclistButton->setTextAlignment(Qt::AlignHCenter);
cyclistButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
connect(contentsWidget,
SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem *)),
this, SLOT(changePage(QListWidgetItem *, QListWidgetItem*)));
connect(saveButton, SIGNAL(clicked()), this, SLOT(save_Clicked()));
}
void ConfigDialog::createNewRange()
{
}
void ConfigDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous)
{
if (!current)
current = previous;
pagesWidget->setCurrentIndex(contentsWidget->row(current));
}
// if save is clicked, we want to:
// new mode: create a new zone starting at the selected date (which may be null, implying BEGIN
// ! new mode: change the CP associated with the present mode
void ConfigDialog::save_Clicked()
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
settings->setValue(GC_UNIT, configPage->unitCombo->currentText());
settings->setValue(GC_ALLRIDES_ASCENDING, configPage->allRidesAscending->checkState());
settings->setValue(GC_CRANKLENGTH, configPage->crankLengthCombo->currentText());
settings->setValue(GC_BIKESCOREDAYS, configPage->BSdaysEdit->text());
settings->setValue(GC_BIKESCOREMODE, configPage->bsModeCombo->currentText());
// if the CP text entry reads invalid, there's nothing we can do
int cp = cyclistPage->getCP();
if (cp == 0) {
QMessageBox::warning(this, tr("Invalid CP"), "Please enter valid CP and try again.");
cyclistPage->setCPFocus();
return;
}
// if for some reason we have no zones yet, then create them
int range;
assert(zones);
if (! *zones) {
*zones = new Zones();
range = -1;
}
else
// determine the current range
range = cyclistPage->getCurrentRange();
// if this is new mode, or if no zone ranges are yet defined, set up the new range
if ((range == -1) || (cyclistPage->isNewMode()))
cyclistPage->setCurrentRange(range = (*zones)->insertRangeAtDate(cyclistPage->selectedDate(), cp));
else
(*zones)->setCP(range, cyclistPage->getText().toInt());
(*zones)->setZonesFromCP(range);
// update the "new zone" checkbox to visible and unchecked
cyclistPage->checkboxNew->setChecked(Qt::Unchecked);
cyclistPage->checkboxNew->setEnabled(true);
(*zones)->write(home);
}
void ConfigDialog::moveCalendarToCurrentRange() {
int range = cyclistPage->getCurrentRange();
if (range < 0)
return;
QDate date;
// put the cursor at the beginning of the selected range if it's not the first
if (range > 0)
date = (*zones)->getStartDate(cyclistPage->getCurrentRange());
// unless the range is the first range, in which case it goes at the end of that range
// use JulianDay to subtract one day from the end date, which is actually the first
// day of the following range
else
date = QDate::fromJulianDay((*zones)->getEndDate(cyclistPage->getCurrentRange()).toJulianDay() - 1);
cyclistPage->setSelectedDate(date);
}
void ConfigDialog::back_Clicked()
{
QDate date;
cyclistPage->setCurrentRange(cyclistPage->getCurrentRange() - 1);
moveCalendarToCurrentRange();
}
void ConfigDialog::forward_Clicked()
{
QDate date;
cyclistPage->setCurrentRange(cyclistPage->getCurrentRange() + 1);
moveCalendarToCurrentRange();
}
void ConfigDialog::delete_Clicked() {
int range = cyclistPage->getCurrentRange();
int num_ranges = (*zones)->getRangeSize();
assert (num_ranges > 1);
QMessageBox msgBox;
msgBox.setText(
tr("Are you sure you want to delete the zone range\n"
"from %1 to %2?\n"
"(%3 range will extend to this date range):") .
arg((*zones)->getStartDateString(cyclistPage->getCurrentRange())) .
arg((*zones)->getEndDateString(cyclistPage->getCurrentRange())) .
arg((range > 0) ? "previous" : "next")
);
QPushButton *deleteButton = msgBox.addButton(tr("Delete"),QMessageBox::YesRole);
msgBox.setStandardButtons(QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
msgBox.setIcon(QMessageBox::Critical);
msgBox.exec();
if(msgBox.clickedButton() == deleteButton)
cyclistPage->setCurrentRange((*zones)->deleteRange(range));
(*zones)->write(home);
}
void ConfigDialog::calendarDateChanged() {
int range = (*zones)->whichRange(cyclistPage->selectedDate());
assert(range >= 0);
cyclistPage->setCurrentRange(range);
}

50
src/ConfigDialog.h Normal file
View File

@@ -0,0 +1,50 @@
#ifndef CONFIGDIALOG_H
#define CONFIGDIALOG_H
#include <QDialog>
#include <QSettings>
#include "Pages.h"
#include "MainWindow.h"
class QListWidget;
class QListWidgetItem;
class QStackedWidget;
class Zones;
class ConfigDialog : public QDialog
{
Q_OBJECT
public:
ConfigDialog(QDir home, Zones **zones);
~ConfigDialog();
public slots:
void changePage(QListWidgetItem *current, QListWidgetItem *previous);
void save_Clicked();
void back_Clicked();
void forward_Clicked();
void delete_Clicked();
void calendarDateChanged();
private:
void createIcons();
void calculateZones();
void createNewRange();
void moveCalendarToCurrentRange();
ConfigurationPage *configPage;
CyclistPage *cyclistPage;
QPushButton *saveButton;
QStackedWidget *pagesWidget;
QPushButton *closeButton;
QHBoxLayout *horizontalLayout;
QHBoxLayout *buttonsLayout;
QVBoxLayout *mainLayout;
QListWidget *contentsWidget;
QSettings *settings;
QDir home;
Zones **zones;
};
#endif

815
src/CpintPlot.cpp Normal file
View File

@@ -0,0 +1,815 @@
/*
* 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 "Zones.h"
#include "CpintPlot.h"
#include <assert.h>
#include <unistd.h>
#include <QDebug>
#include <qwt_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include "RideItem.h"
#include "LogTimeScaleDraw.h"
#include "LogTimeScaleEngine.h"
#include "RideFile.h"
CpintPlot::CpintPlot(
QString p
) :
progress(NULL),
needToScanRides(true),
path(p),
thisCurve(NULL),
CPCurve(NULL),
grid(NULL),
zones(NULL)
{
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setAxisTitle(yLeft, "Average Power (watts)");
setAxisTitle(xBottom, "Interval Length");
setAxisScaleDraw(xBottom, new LogTimeScaleDraw);
setAxisScaleEngine(xBottom, new LogTimeScaleEngine);
setAxisScale(xBottom, 1.0 / 60.0, 60);
grid = new QwtPlotGrid();
grid->enableX(false);
QPen gridPen;
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
grid->attach(this);
allCurves= QList <QwtPlotCurve *>::QList();
allZoneLabels= QList <QwtPlotMarker *>::QList();
}
struct cpi_file_info {
QString file, inname, outname;
};
bool
is_ride_filename(const QString filename)
{
QRegExp re("^([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|srm|csv|tcx|hrm|wko)$");
return (re.exactMatch(filename));
}
QString
ride_filename_to_cpi_filename(const QString filename)
{
return (QFileInfo(filename).completeBaseName() + ".cpi");
}
static void
cpi_files_to_update(const QDir &dir, QList<cpi_file_info> &result)
{
QStringList filenames = RideFileFactory::instance().listRideFiles(dir);
QListIterator<QString> i(filenames);
while (i.hasNext()) {
const QString &filename = i.next();
if (is_ride_filename(filename)) {
QString inname = dir.absoluteFilePath(filename);
QString outname =
dir.absoluteFilePath(ride_filename_to_cpi_filename(filename));
QFileInfo ifi(inname), ofi(outname);
if (!ofi.exists() || (ofi.lastModified() < ifi.lastModified())) {
cpi_file_info info;
info.file = filename;
info.inname = inname;
info.outname = outname;
result.append(info);
}
}
}
}
struct cpint_point
{
double secs;
int watts;
cpint_point(double s, int w) : secs(s), watts(w) {}
};
struct cpint_data {
QStringList errors;
QList<cpint_point> points;
int rec_int_ms;
cpint_data() : rec_int_ms(0) {}
};
static void
update_cpi_file(const cpi_file_info *info, QProgressDialog *progress,
double &progress_sum, double progress_max)
{
QFile file(info->inname);
QStringList errors;
RideFile *rideFile =
RideFileFactory::instance().openRideFile(file, errors);
if (! rideFile)
return;
cpint_data data;
data.rec_int_ms = (int) round(rideFile->recIntSecs() * 1000.0);
QListIterator<RideFilePoint*> i(rideFile->dataPoints());
while (i.hasNext()) {
const RideFilePoint *p = i.next();
double secs = round(p->secs * 1000.0) / 1000;
data.points.append(cpint_point(secs, (int) round(p->watts)));
}
delete rideFile;
FILE *out = fopen(info->outname.toAscii().constData(), "w");
assert(out);
int total_secs = (int) ceil(data.points.back().secs);
// don't allow data more than one week
#define SECONDS_PER_WEEK 7 * 24 * 60 * 60
if (total_secs > SECONDS_PER_WEEK) {
fclose(out);
return;
}
QVector <double> ride_bests(total_secs + 1); // was calloc'ed array (unfreed?), changed djconnel
// initialize ride_bests
for (int i = 0; i < ride_bests.size(); i ++)
ride_bests[i] = 0.0;
bool canceled = false;
int progress_count = 0;
for (int i = 0; i < data.points.size() - 1; ++i) {
cpint_point *p = &data.points[i];
double sum = 0.0;
double prev_secs = p->secs;
for (int j = i + 1; j < data.points.size(); ++j) {
cpint_point *q = &data.points[j];
if (++progress_count % 1000 == 0) {
double x = (progress_count + progress_sum)
/ progress_max * 1000.0;
// Use min() just in case math is wrong...
int n = qMin((int) round(x), 1000);
progress->setValue(n);
QCoreApplication::processEvents();
if (progress->wasCanceled()) {
canceled = true;
goto done;
}
}
sum += data.rec_int_ms / 1000.0 * q->watts;
double dur_secs = q->secs - p->secs;
double avg = sum / dur_secs;
int dur_secs_top = (int) floor(dur_secs);
int dur_secs_bot =
qMax((int) floor(dur_secs - data.rec_int_ms / 1000.0), 0);
for (int k = dur_secs_top; k > dur_secs_bot; --k) {
if (ride_bests[k] < avg)
ride_bests[k] = avg;
}
prev_secs = q->secs;
}
}
// avoid decreasing work with increasing duration
{
double maxwork = 0.0;
for (int i = 1; i <= total_secs; ++i) {
// note index is being used here in lieu of time, as the index
// is assumed to be proportional to time
double work = ride_bests[i] * i;
if (maxwork > work)
ride_bests[i] = round(maxwork / i);
else
maxwork = work;
if (ride_bests[i] != 0)
fprintf(out, "%6.3f %3.0f\n", i / 60.0, round(ride_bests[i]));
}
}
done:
fclose(out);
if (canceled)
unlink(info->outname.toAscii().constData());
progress_sum += progress_count;
}
QDate
cpi_filename_to_date(const QString filename) {
QRegExp rx(".*([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])\\.cpi$");
if (rx.exactMatch(filename)) {
assert(rx.numCaptures() == 6);
QDate date = QDate(
rx.cap(1).toInt(),
rx.cap(2).toInt(),
rx.cap(3).toInt()
);
if (! date.isValid()) {
return QDate();
}
else
return date;
}
else
return QDate(); // return value was 1 Jan: changed to null
}
static int
read_one(const char *inname, QVector<double> &bests, QVector<QDate> &bestDates, QHash <QString, bool> *cpiDataInBests)
{
FILE *in = fopen(inname, "r");
if (!in)
return -1;
int lineno = 1;
char line[40];
while (fgets(line, sizeof(line), in) != NULL) {
double mins;
int watts;
if (sscanf(line, "%lf %d\n", &mins, &watts) != 2) {
fprintf(stderr, "Bad match on line %d: %s", lineno, line);
exit(1);
}
int secs = (int) round(mins * 60.0);
if (secs >= bests.size()) {
bests.resize(secs + 1);
bestDates.resize(secs + 1);
}
if (bests[secs] < watts){
bests[secs] = watts;
bestDates[secs] = cpi_filename_to_date(QString(inname));
// mark the filename as having contributed to the bests
// Note this contribution may subsequently be over-written, so
// for example the first file scanned will always be tagged.
if (cpiDataInBests)
(*cpiDataInBests)[inname] = true;
}
++lineno;
}
fclose(in);
return 0;
}
static int
read_cpi_file(const QDir &dir, const QFileInfo &raw, QVector<double> &bests, QVector<QDate> &bestDates, QHash <QString, bool> *cpiDataInBests)
{
QString inname = dir.absoluteFilePath(raw.completeBaseName() + ".cpi");
return read_one(inname.toAscii().constData(), bests, bestDates, cpiDataInBests);
}
// extract critical power parameters which match the given curve
// model: maximal power = cp (1 + tau / [t + t0]), where t is the
// duration of the effort, and t, cp and tau are model parameters
// the basic critical power model is t0 = 0, but non-zero has
// been discussed in the literature
// it is assumed duration = index * seconds
void
CpintPlot::deriveCPParameters()
{
// bounds on anaerobic interval in minutes
const double t1 = USE_T0_IN_CP_MODEL ? 0.25 : 1;
const double t2 = 6;
// bounds on aerobic interval in minutes
const double t3 = 10;
const double t4 = 60;
// bounds of these time valus in the data
int i1, i2, i3, i4;
// find the indexes associated with the bounds
// the first point must be at least the minimum for the anaerobic interval, or quit
for (i1 = 0; i1 < 60 * t1; i1++)
if (i1 + 1 >= bests.size())
return;
// the second point is the maximum point suitable for anaerobicly dominated efforts.
for (i2 = i1; i2 + 1 <= 60 * t2; i2++)
if (i2 + 1 >= bests.size())
return;
// the third point is the beginning of the minimum duration for aerobic efforts
for (i3 = i2; i3 < 60 * t3; i3++)
if (i3 + 1 >= bests.size())
return;
for (i4 = i3; i4 + 1 <= 60 * t4; i4++)
if (i4 + 1 >= bests.size())
break;
// initial estimate of tau
if (tau == 0)
tau = 1;
// initial estimate of cp (if not already available)
if (cp == 0)
cp = 300;
// initial estimate of t0: start small to maximize sensitivity to data
t0 = 0;
// lower bound on tau
const double tau_min = 0.5;
// convergence delta for tau
const double tau_delta_max = 1e-4;
const double t0_delta_max = 1e-4;
// previous loop value of tau and t0
double tau_prev;
double t0_prev;
// maximum number of loops
const int max_loops = 100;
// loop to convergence
int iteration = 0;
do {
if (iteration ++ > max_loops) {
fprintf(stderr, "maximum number of loops %d exceeded in cp model extraction\n", max_loops);
break;
}
// record the previous version of tau, for convergence
tau_prev = tau;
t0_prev = t0;
// estimate cp, given tau
int i;
cp = 0;
for (i = i3; i <= i4; i++) {
double cpn = bests[i] / (1 + tau / (t0 + i / 60.0));
if (cp < cpn)
cp = cpn;
}
// if cp = 0; no valid data; give up
if (cp == 0.0)
return;
// estimate tau, given cp
tau = tau_min;
for (i = i1; i <= i2; i++) {
double taun = (bests[i] / cp - 1) * (i / 60.0 + t0) - t0;
if (tau < taun)
tau = taun;
}
// update t0 if we're using that model
#if USE_T0_IN_CP_MODEL
t0 = tau / (bests[1] / cp - 1) - 1 / 60.0;
#endif
// the following line is debugging code and can be removed
fprintf(stderr, "%d: tau = %.2f; cp = %.2f; t0 = %.2f\n", iteration, tau, cp, t0);
} while ((fabs(tau - tau_prev) > tau_delta_max) ||
(fabs(t0 - t0_prev) > t0_delta_max)
);
}
void
CpintPlot::plot_CP_curve(
CpintPlot *thisPlot, // the plot we're currently displaying
double cp,
double tau,
double t0
) {
// detach the CP curve if it exists
if (CPCurve)
CPCurve->detach();
// if there's no cp, then there's nothing to do
if (cp <= 0)
return;
// populate curve data with a CP curve
const int curve_points = 100;
double tmin = USE_T0_IN_CP_MODEL ? 1.0/60 : tau;
double tmax = 180.0;
double cp_curve_power[curve_points];
double cp_curve_time[curve_points];
int i;
for (i = 0; i < curve_points; i ++) {
double x = (double) i / (curve_points - 1);
double t = pow(tmax, x) * pow(tmin, 1-x);
cp_curve_time[i] = t;
cp_curve_power[i] = cp * (1 + tau / (t + t0));
}
// generate a plot
QString curve_title;
#if USE_T0_IN_CP_MODEL
curve_title.sprintf("CP=%.1f W; AWC/CP=%.2f m; t0=%.1f s", cp, tau, 60 * t0);
#else
curve_title.sprintf("CP=%.1f W; AWC/CP=%.2f m", cp, tau);
#endif
CPCurve = new QwtPlotCurve(curve_title);
CPCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen *pen = new QPen(Qt::red);
pen->setWidth(2.0);
pen->setStyle(Qt::DashLine);
CPCurve->setPen(*pen);
CPCurve->setData(
cp_curve_time,
cp_curve_power,
curve_points
);
CPCurve->attach(thisPlot);
delete pen;
return;
}
void CpintPlot::clear_CP_Curves() {
// unattach any existing shading curves and reset the list
if (allCurves.size()) {
QListIterator<QwtPlotCurve *> i(allCurves);
while (i.hasNext()) {
QwtPlotCurve *curve = i.next();
if (curve) {
curve->detach();
delete curve;
}
}
allCurves.clear();
}
// now delete any labels
if (allZoneLabels.size()) {
QListIterator<QwtPlotMarker *> i(allZoneLabels);
while (i.hasNext()) {
QwtPlotMarker *label = i.next();
if (label) {
label->detach();
delete label;
}
}
allZoneLabels.clear();
}
}
void
CpintPlot::plot_allCurve (
CpintPlot *thisPlot,
int n_values,
const double *power_values
) {
clear_CP_Curves();
QVector<double> time_values(n_values);
// generate an array of time values
//double time_values[n_values];
for (int t = 1; t <= n_values; t++)
time_values[t - 1] = t / 60.0;
// generate zones from derived CP value
if (cp > 0) {
QList <int> power_zone;
int n_zones = (*zones)->lowsFromCP(&power_zone, (int) int(cp));
QList <int> n_zone;
// the lowest zone goes to zero power, so mark its start at the last data point
n_zone.append(n_values - 1);
// start the search at the next-to-lowest zone
int z = 1;
// search the maximal power curve to extract the zone times
for (int i = n_values; i-- > 0;) {
// if we reach the beginning of the curve OR if we hit a zone boundary, we're done with the present zone
if ((i == 0) || (power_values[i] > power_zone[z])) {
n_zone.append(
(z == n_zones) ?
0 :
(
(
(i == n_values - 1) ||
(abs(power_values[i] - power_zone[z]) < abs(power_zone[z] - power_values[i + 1]))
) ?
i :
i + 1
)
);
// draw curves for the zone we're leaving, if it spans any segments
if (n_zone[z - 1] > n_zone[z]) {
// define the individual code segments. Note in the old code with a single segment, it was
// part of the class. This curve is not a protected member of the class. djconnel Apr2009
QwtPlotCurve *curve;
curve =
new QwtPlotCurve((*zones)->getDefaultZoneName(z - 1));
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen *pen = new QPen(zoneColor(z - 1, n_zones));
pen->setWidth(2.0);
curve->setPen(*pen);
curve->attach(thisPlot);
QColor brush_color = zoneColor(z - 1, n_zones);
brush_color.setAlpha(64);
curve->setBrush(brush_color); // brush fills below the line
curve->setData(
time_values.data() + n_zone[z],
power_values + n_zone[z],
n_zone[z - 1] - n_zone[z] + 1
);
delete pen;
// add the curve to the list
allCurves.append(curve);
// render a colored label on the zone
QwtText text((*zones)->getDefaultZoneName(z - 1));
text.setFont(QFont("Helvetica",24, QFont::Bold));
QColor text_color = zoneColor(z - 1, n_zones);
text_color.setAlpha(128);
text.setColor(text_color);
QwtPlotMarker *label_mark;
label_mark = new QwtPlotMarker();
// place the text in the geometric mean in time, at a decent power
label_mark->setValue(
sqrt(time_values[n_zone[z-1]] * time_values[n_zone[z]]),
(power_values[n_zone[z-1]] + power_values[n_zone[z]]) / 5
);
label_mark->setLabel(text);
label_mark->attach(thisPlot);
allZoneLabels.append(label_mark);
}
if (z < n_zones)
fprintf(stderr, "zone %s: %d watts, index = %d\n",
(*zones)->getDefaultZoneName(z).toAscii().constData(),
power_zone[z],
n_zone[z]
);
// if we're to the smallest time, we're done
if (i == 0)
break;
// increment zone number
if (z < n_zones)
z ++;
// if we're to the final zone, just jump to the beginning of the plot: we're done
if (z == n_zones)
i = 1;
// else, we've got to recheck this point for the next zone
else
i ++;
}
}
}
// no zones available: just plot the curve without zones
else {
QwtPlotCurve *curve;
curve = new QwtPlotCurve("maximal power");
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen *pen = new QPen(Qt::red);
pen->setWidth(2.0);
curve->setPen(*pen);
QColor brush_color = Qt::red;
brush_color.setAlpha(64);
curve->setBrush(brush_color); // brush fills below the line
curve->setData(
time_values.data(),
power_values,
n_values
);
curve->attach(thisPlot);
delete pen;
allCurves.append(curve);
}
// set the x-axis to span the time of the all-time curve, starting at 1 second
thisPlot->setAxisScale(
thisPlot->xBottom,
1.0 / 60,
time_values[n_values - 1]
);
// set the y-axis to go from zero to the maximum power, rounded up to nearest 100 watts
thisPlot->setAxisScale(
thisPlot->yLeft,
0,
100 * ceil( power_values[0] / 100 )
);
}
void
CpintPlot::calculate(RideItem *rideItem)
{
QString fileName = rideItem->fileName;
QDateTime dateTime = rideItem->dateTime;
QDir dir(path);
QFileInfo file(fileName);
zones = rideItem->zones;
if (needToScanRides) {
bests.clear();
bestDates.clear();
cpiDataInBests.clear();
if (CPCurve) {
CPCurve->detach();
CPCurve = NULL;
}
fflush(stderr);
bool aborted = false;
QList<cpi_file_info> to_update;
cpi_files_to_update(dir, to_update);
double progress_max = 0.0;
if (!to_update.empty()) {
QListIterator<cpi_file_info> i(to_update);
while (i.hasNext()) {
const cpi_file_info &info = i.next();
QFile file(info.inname);
QStringList errors;
RideFile *rideFile =
RideFileFactory::instance().openRideFile(file, errors);
if (rideFile) {
double x = rideFile->dataPoints().size();
progress_max += x * (x + 1.0) / 2.0;
delete rideFile;
}
}
}
progress = new QProgressDialog(
QString(tr("Computing critical power intervals.\n"
"This may take a while.\n")),
tr("Abort"), 0, 1000, this);
double progress_sum = 0.0;
int endingOffset = progress->labelText().size();
if (!to_update.empty()) {
QListIterator<cpi_file_info> i(to_update);
int count = 1;
while (i.hasNext()) {
const cpi_file_info &info = i.next();
QString existing = progress->labelText();
existing.chop(progress->labelText().size() - endingOffset);
progress->setLabelText(
existing + QString(tr("Processing %1...")).arg(info.file));
progress->setValue(count++);
update_cpi_file(&info, progress, progress_sum, progress_max);
QCoreApplication::processEvents();
if (progress->wasCanceled()) {
aborted = true;
break;
}
}
}
if (!aborted) {
QString existing = progress->labelText();
existing.chop(progress->labelText().size() - endingOffset);
QStringList filters;
filters << "*.cpi";
QStringList list = dir.entryList(filters, QDir::Files, QDir::Name);
progress->setLabelText(
existing + tr("Aggregating over all files."));
progress->setRange(0, list.size());
progress->setValue(0);
progress->show();
QListIterator<QString> i(list);
while (i.hasNext()) {
const QString &filename = i.next();
QString path = dir.absoluteFilePath(filename);
read_one(path.toAscii().constData(), bests, bestDates, &cpiDataInBests);
progress->setValue(progress->value() + 1);
QCoreApplication::processEvents();
if (progress->wasCanceled()) {
aborted = true;
break;
}
}
}
if (!aborted && bests.size()) {
int maxNonZero = 0;
// check that total work doesn't decrease with time
double maxwork = 0.0;
for (int i = 0; i < bests.size(); ++i) {
// record the date associated with each point's CPI file,
if (bests[i] > 0)
maxNonZero = i;
// note index is being used here in lieu of time, as the index
// is assumed to be proportional to time
double work = bests[i] * i;
if ((i > 0) && (maxwork > work)) {
bests[i] = round(maxwork / i);
bestDates[i] = bestDates[i - 1];
}
else
maxwork = work;
}
// derive CP model
if (bests.size() > 1) {
// cp model parameters
cp = 0;
tau = 0;
t0 = 0;
// calculate CP model from all-time best data
deriveCPParameters();
plot_CP_curve(this, cp, tau, t0);
plot_allCurve(this, maxNonZero - 1, bests.constData() + 1);
}
needToScanRides = false;
}
delete progress;
progress = NULL;
}
if (!needToScanRides) {
if (thisCurve)
delete thisCurve;
thisCurve = NULL;
QVector<double> bests;
QVector<QDate> bestDates;
if ((read_cpi_file(dir, file, bests, bestDates, NULL) == 0) && bests.size()) {
double *timeArray = new double[bests.size()];
int maxNonZero = 0;
for (int i = 0; i < bests.size(); ++i) {
timeArray[i] = i / 60.0;
if (bests[i] > 0) maxNonZero = i;
}
if (maxNonZero > 1) {
thisCurve = new QwtPlotCurve(
dateTime.toString("ddd MMM d, yyyy h:mm AP"));
thisCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen *pen = new QPen(Qt::black);
pen->setWidth(2.0);
thisCurve->setPen(QPen(Qt::black));
thisCurve->attach(this);
thisCurve->setData(timeArray + 1, bests.constData() + 1,
maxNonZero - 1);
}
delete [] timeArray;
}
}
replot();
}
// delete a CPI file
bool
CpintPlot::deleteCpiFile(QString filename)
{
// first, get ride of the file
if (! QFile::remove(filename))
return false;
// now check to see if this file contributed to the bests
// in the current implementation a false means it does
// not contribute, but a true only means it at one time
// contributed (may not in the end).
if (cpiDataInBests.contains(filename)) {
if (cpiDataInBests[filename])
needToScanRides = true;
cpiDataInBests.remove(filename);
}
return true;
}
void
CpintPlot::showGrid(int state)
{
assert(state != Qt::PartiallyChecked);
grid->setVisible(state == Qt::Checked);
replot();
}

93
src/CpintPlot.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* 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_CpintPlot_h
#define _GC_CpintPlot_h 1
#include "Zones.h"
#include <qwt_plot.h>
#include <qwt_plot_marker.h> // added djconnel 06Apr2009
#include <QtGui>
#include <QHash>
class QwtPlotCurve;
class QwtPlotGrid;
class RideItem;
#define USE_T0_IN_CP_MODEL 0 // added djconnel 08Apr2009: allow 3-parameter CP model
bool is_ride_filename(const QString filename);
QString ride_filename_to_cpi_filename(const QString filename);
QDate cpi_filename_to_date(const QString filename);
class CpintPlot : public QwtPlot
{
Q_OBJECT
public:
CpintPlot(QString path);
QProgressDialog *progress;
bool needToScanRides;
const QwtPlotCurve *getThisCurve() const { return thisCurve; }
QVector<QDate> getBestDates() { return bestDates; }
QVector<double> getBests() { return bests; }
double cp, tau, t0; // CP model parameters
void deriveCPParameters(); // derive the CP model parameters
bool deleteCpiFile(QString filename); // delete a CPI file and clean up
public slots:
void showGrid(int state);
void calculate(RideItem *rideItem);
void plot_CP_curve(
CpintPlot *plot,
double cp,
double tau,
double t0n
);
void plot_allCurve(
CpintPlot *plot,
int n_values,
const double *power_values
);
protected:
QString path;
QwtPlotCurve *thisCurve;
QwtPlotCurve *CPCurve;
QList <QwtPlotCurve *> allCurves;
QList <QwtPlotMarker *> allZoneLabels;
void clear_CP_Curves();
QwtPlotGrid *grid;
QVector<double> bests;
QVector<QDate> bestDates;
Zones **zones; // pointer to power zones added djconnel 24Apr2009
QHash <QString, bool> cpiDataInBests; // hash: keys are CPI files contributing to bests (at least originally)
};
#endif // _GC_CpintPlot_h

248
src/CsvRideFile.cpp Normal file
View File

@@ -0,0 +1,248 @@
/*
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net),
* Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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 "CsvRideFile.h"
#include <QRegExp>
#include <QTextStream>
#include <algorithm> // for std::sort
#include <assert.h>
#include "math.h"
#define MILES_TO_KM 1.609344
static int csvFileReaderRegistered =
RideFileFactory::instance().registerReader("csv", new CsvFileReader());
RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
{
QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive);
QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive);
bool metric;
// TODO: a more robust regex for ergomo files
// i don't have an example with english headers
// the ergomo CSV has two rows of headers. units are on the second row.
/*
ZEIT,STRECKE,POWER,RPM,SPEED,PULS,HÖHE,TEMP,INTERVAL,PAUSE
L_SEC,KM,WATT,RPM,KM/H,BPM,METER,°C,NUM,SEC
*/
QRegExp ergomoCSV("(ZEIT|STRECKE)", Qt::CaseInsensitive);
bool ergomo = false;
int unitsHeader = 1;
int total_pause = 0;
int currentInterval = 0;
int prevInterval = 0;
// TODO: with all these formats, should the logic change to a switch/case structure?
// The iBike format CSV file has five lines of headers (data begins on line 6)
// starting with:
/*
iBike,8,english
2008,8,8,6,32,52
{Various configuration data, recording interval at line[4][4]}
Speed (mph),Wind Speed (mph),Power (W),Distance (miles),Cadence (RPM),Heartrate (BPM),Elevation (feet),Hill slope (%),Internal,Internal,Internal,DFPM Power,Latitude,Longitude
*/
// Modified the regExp string to allow for 2-digit version numbers - 23 Mar 2009, thm
QRegExp iBikeCSV("iBike,\\d\\d?,[a-z]+", Qt::CaseInsensitive);
bool iBike = false;
int recInterval;
if (!file.open(QFile::ReadOnly)) {
errors << ("Could not open ride file: \""
+ file.fileName() + "\"");
return NULL;
}
int lineno = 1;
QTextStream is(&file);
RideFile *rideFile = new RideFile();
while (!is.atEnd()) {
// the readLine() method doesn't handle old Macintosh CR line endings
// this workaround will load the the entire file if it has CR endings
// then split and loop through each line
// otherwise, there will be nothing to split and it will read each line as expected.
QString linesIn = is.readLine();
QStringList lines = linesIn.split('\r');
// workaround for empty lines
if(lines.isEmpty()) {
lineno++;
continue;
}
for (int li = 0; li < lines.size(); ++li) {
QString line = lines[li];
if (lineno == 1) {
if (ergomoCSV.indexIn(line) != -1) {
ergomo = true;
rideFile->setDeviceType("Ergomo CSV");
unitsHeader = 2;
++lineno;
continue;
}
else {
if(iBikeCSV.indexIn(line) != -1) {
iBike = true;
rideFile->setDeviceType("iBike CSV");
unitsHeader = 5;
++lineno;
continue;
}
rideFile->setDeviceType("PowerTap CSV");
}
}
if (iBike && lineno == 4) {
// this is the line with the iBike configuration data
// recording interval is in the [4] location (zero-based array)
// the trailing zeroes in the configuration area seem to be causing an error
// the number is in the format 5.000000
recInterval = (int)line.section(',',4,4).toDouble();
}
if (lineno == unitsHeader) {
if (metricUnits.indexIn(line) != -1)
metric = true;
else if (englishUnits.indexIn(line) != -1)
metric = false;
else {
errors << "Can't find units in first line: \"" + line + "\" of file \"" + file.fileName() + "\".";
delete rideFile;
file.close();
return NULL;
}
}
else if (lineno > unitsHeader) {
double minutes,nm,kph,watts,km,cad,alt,hr;
int interval;
int pause;
if (!ergomo && !iBike) {
minutes = line.section(',', 0, 0).toDouble();
nm = line.section(',', 1, 1).toDouble();
kph = line.section(',', 2, 2).toDouble();
watts = line.section(',', 3, 3).toDouble();
km = line.section(',', 4, 4).toDouble();
cad = line.section(',', 5, 5).toDouble();
hr = line.section(',', 6, 6).toDouble();
interval = line.section(',', 7, 7).toInt();
alt = line.section(',', 8, 8).toDouble();
if (!metric) {
km *= MILES_TO_KM;
kph *= MILES_TO_KM;
}
}
else if (iBike) {
// this must be iBike
// can't find time as a column.
// will we have to extrapolate based on the recording interval?
// reading recording interval from config data in ibike csv file
minutes = (recInterval * lineno - unitsHeader)/60.0;
nm = NULL; //no torque
kph = line.section(',', 0, 0).toDouble();
watts = line.section(',', 2, 2).toDouble();
km = line.section(',', 3, 3).toDouble();
cad = line.section(',', 4, 4).toDouble();
hr = line.section(',', 5, 5).toDouble();
interval = NULL; //not provided?
if (!metric) {
km *= MILES_TO_KM;
kph *= MILES_TO_KM;
}
}
else {
// for ergomo formatted CSV files
minutes = line.section(',', 0, 0).toDouble() + total_pause;
km = line.section(',', 1, 1).toDouble();
watts = line.section(',', 2, 2).toDouble();
cad = line.section(',', 3, 3).toDouble();
kph = line.section(',', 4, 4).toDouble();
hr = line.section(',', 5, 5).toDouble();
alt = line.section(',', 6, 6).toDouble();
interval = line.section(',', 8, 8).toInt();
if (interval != prevInterval) {
prevInterval = interval;
if (interval != 0) currentInterval++;
}
if (interval != 0) interval = currentInterval;
pause = line.section(',', 9, 9).toInt();
total_pause += pause;
nm = NULL; // torque is not provided in the Ergomo file
// the ergomo records the time in whole seconds
// RECORDING INT. 1, 2, 5, 10, 15 or 30 per sec
// Time is *always* perfectly sequential. To find pauses,
// you need to read the PAUSE column.
minutes = minutes/60.0;
if (!metric) {
km *= MILES_TO_KM;
kph *= MILES_TO_KM;
}
}
// PT reports no data as watts == -1.
if (watts == -1)
watts = 0;
rideFile->appendPoint(minutes * 60.0, cad, hr, km,
kph, nm, watts, alt, interval);
}
++lineno;
}
}
file.close();
// To estimate the recording interval, take the median of the
// first 1000 samples and round to nearest millisecond.
int n = rideFile->dataPoints().size();
n = qMin(n, 1000);
if (n >= 2) {
double *secs = new double[n-1];
for (int i = 0; i < n-1; ++i) {
double now = rideFile->dataPoints()[i]->secs;
double then = rideFile->dataPoints()[i+1]->secs;
secs[i] = then - now;
}
std::sort(secs, secs + n - 1);
int mid = n / 2 - 1;
double recint = round(secs[mid] * 1000.0) / 1000.0;
rideFile->setRecIntSecs(recint);
}
// less than 2 data points is not a valid ride file
else {
errors << "Insufficient valid data in file \"" + file.fileName() + "\".";
delete rideFile;
file.close();
return NULL;
}
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$");
if (rideTime.indexIn(file.fileName()) >= 0) {
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
rideTime.cap(2).toInt(),
rideTime.cap(3).toInt()),
QTime(rideTime.cap(4).toInt(),
rideTime.cap(5).toInt(),
rideTime.cap(6).toInt()));
rideFile->setStartTime(datetime);
}
return rideFile;
}

29
src/CsvRideFile.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2007 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 _CsvRideFile_h
#define _CsvRideFile_h
#include "RideFile.h"
struct CsvFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _CsvRideFile_h

231
src/D2XX.cpp Normal file
View File

@@ -0,0 +1,231 @@
/*
* Copyright (c) 2008 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 "D2XX.h"
#include <dlfcn.h>
// D2XXWrapper is a wrapper around libftd2xx to make it amenable to loading
// with dlopen().
#define LOAD_SYM(type,var,name) \
var = (type*) dlsym(handle, name); \
if (!var) { \
error = QString("could not load symbol ") + name; \
return false; \
}
#ifdef WIN32
#define WIN32_STDCALL __stdcall
#else
#define WIN32_STDCALL
#endif
typedef FT_STATUS WIN32_STDCALL FP_OpenEx(PVOID pArg1, DWORD Flags, FT_HANDLE *pHandle);
typedef FT_STATUS WIN32_STDCALL FP_Close(FT_HANDLE ftHandle);
typedef FT_STATUS WIN32_STDCALL FP_SetBaudRate(FT_HANDLE ftHandle, ULONG BaudRate);
typedef FT_STATUS WIN32_STDCALL FP_SetDataCharacteristics(FT_HANDLE ftHandle, UCHAR WordLength, UCHAR StopBits, UCHAR Parity);
typedef FT_STATUS WIN32_STDCALL FP_SetFlowControl(FT_HANDLE ftHandle, USHORT FlowControl, UCHAR XonChar, UCHAR XoffChar);
typedef FT_STATUS WIN32_STDCALL FP_GetQueueStatus(FT_HANDLE ftHandle, DWORD *dwRxBytes);
typedef FT_STATUS WIN32_STDCALL FP_SetTimeouts(FT_HANDLE ftHandle, ULONG ReadTimeout, ULONG WriteTimeout);
typedef FT_STATUS WIN32_STDCALL FP_Read(FT_HANDLE ftHandle, LPVOID lpBuffer, DWORD nBufferSize, LPDWORD lpBytesReturned);
typedef FT_STATUS WIN32_STDCALL FP_Write(FT_HANDLE ftHandle, LPVOID lpBuffer, DWORD nBufferSize, LPDWORD lpBytesWritten);
typedef FT_STATUS WIN32_STDCALL FP_CreateDeviceInfoList(LPDWORD lpdwNumDevs);
typedef FT_STATUS WIN32_STDCALL FP_GetDeviceInfoList(FT_DEVICE_LIST_INFO_NODE *pDest, LPDWORD lpdwNumDevs);
struct D2XXWrapper {
void *handle;
FP_OpenEx *open_ex;
FP_Close *close;
FP_SetBaudRate *set_baud_rate;
FP_SetDataCharacteristics *set_data_characteristics;
FP_SetFlowControl *set_flow_control;
FP_GetQueueStatus *get_queue_status;
FP_SetTimeouts *set_timeouts;
FP_Read *read;
FP_Write *write;
FP_CreateDeviceInfoList *create_device_info_list;
FP_GetDeviceInfoList *get_device_info_list;
D2XXWrapper() : handle(NULL) {}
~D2XXWrapper() { if (handle) dlclose(handle); }
bool init(QString &error)
{
#if defined(Q_OS_LINUX)
const char *libname = "libftd2xx.so";
#elif defined(Q_OS_WIN32)
const char *libname = "ftd2xx.dll";
#elif defined(Q_OS_DARWIN)
const char *libname = "libftd2xx.dylib";
#endif
handle = dlopen(libname, RTLD_NOW);
if (!handle) {
error = QString("Couldn't load library ") + libname + ".";
return false;
}
LOAD_SYM(FP_OpenEx, open_ex, "FT_OpenEx");
LOAD_SYM(FP_Close, close, "FT_Close");
LOAD_SYM(FP_SetBaudRate, set_baud_rate, "FT_SetBaudRate");
LOAD_SYM(FP_SetDataCharacteristics, set_data_characteristics, "FT_SetDataCharacteristics");
LOAD_SYM(FP_SetFlowControl, set_flow_control, "FT_SetFlowControl");
LOAD_SYM(FP_GetQueueStatus, get_queue_status, "FT_GetQueueStatus");
LOAD_SYM(FP_SetTimeouts, set_timeouts, "FT_SetTimeouts");
LOAD_SYM(FP_Read, read, "FT_Read");
LOAD_SYM(FP_Write, write, "FT_Write");
LOAD_SYM(FP_CreateDeviceInfoList, create_device_info_list, "FT_CreateDeviceInfoList");
LOAD_SYM(FP_GetDeviceInfoList, get_device_info_list, "FT_GetDeviceInfoList");
return true;
}
};
static D2XXWrapper *lib; // singleton lib instance
bool D2XXRegistered = CommPort::addListFunction(&D2XX::myListCommPorts);
D2XX::D2XX(const FT_DEVICE_LIST_INFO_NODE &info) :
info(info), isOpen(false)
{
}
D2XX::~D2XX()
{
if (isOpen)
close();
}
bool
D2XX::open(QString &err)
{
assert(!isOpen);
FT_STATUS ftStatus =
lib->open_ex(info.Description, FT_OPEN_BY_DESCRIPTION, &ftHandle);
if (ftStatus != FT_OK) {
err = QString("FT_Open: %1").arg(ftStatus);
return false;
}
isOpen = true;
ftStatus = lib->set_baud_rate(ftHandle, 9600);
if (ftStatus != FT_OK) {
err = QString("FT_SetBaudRate: %1").arg(ftStatus);
close();
}
ftStatus = lib->set_data_characteristics(ftHandle,FT_BITS_8,FT_STOP_BITS_1,
FT_PARITY_NONE);
if (ftStatus != FT_OK) {
err = QString("FT_SetDataCharacteristics: %1").arg(ftStatus);
close();
}
ftStatus = lib->set_flow_control(ftHandle,FT_FLOW_NONE,
'0','0'); //the 0's are ignored
if (ftStatus != FT_OK) {
err = QString("FT_SetFlowControl: %1").arg(ftStatus);
close();
}
return true;
}
void
D2XX::close()
{
assert(isOpen);
lib->close(ftHandle);
isOpen = false;
}
int
D2XX::read(void *buf, size_t nbyte, QString &err)
{
assert(isOpen);
DWORD rxbytes;
FT_STATUS ftStatus = lib->get_queue_status(ftHandle, &rxbytes);
if (ftStatus != FT_OK) {
err = QString("FT_GetQueueStatus: %1").arg(ftStatus);
return -1;
}
// printf("rxbytes=%d\n", (int) rxbytes);
// Return immediately whenever there's something to read.
if (rxbytes > 0 && rxbytes < nbyte)
nbyte = rxbytes;
if (nbyte > rxbytes)
lib->set_timeouts(ftHandle, 5000, 5000);
DWORD n;
ftStatus = lib->read(ftHandle, buf, nbyte, &n);
if (ftStatus == FT_OK)
return n;
err = QString("FT_Read: %1").arg(ftStatus);
return -1;
}
int
D2XX::write(void *buf, size_t nbyte, QString &err)
{
assert(isOpen);
DWORD n;
FT_STATUS ftStatus = lib->write(ftHandle, buf, nbyte, &n);
if (ftStatus == FT_OK)
return n;
err = QString("FT_Write: %1").arg(ftStatus);
return -1;
}
QString
D2XX::name() const
{
return QString("D2XX: ") + info.Description;
}
QVector<CommPortPtr>
D2XX::myListCommPorts(QString &err)
{
QVector<CommPortPtr> result;
if (!lib) {
lib = new D2XXWrapper;
if (!lib->init(err)) {
delete lib;
lib = NULL;
return result;
}
}
DWORD numDevs;
FT_STATUS ftStatus = lib->create_device_info_list(&numDevs);
if(ftStatus != FT_OK) {
err = QString("FT_CreateDeviceInfoList: %1").arg(ftStatus);
return result;
}
FT_DEVICE_LIST_INFO_NODE *devInfo = new FT_DEVICE_LIST_INFO_NODE[numDevs];
ftStatus = lib->get_device_info_list(devInfo, &numDevs);
if (ftStatus != FT_OK)
err = QString("FT_GetDeviceInfoList: %1").arg(ftStatus);
else {
for (DWORD i = 0; i < numDevs; i++)
result.append(CommPortPtr(new D2XX(devInfo[i])));
}
delete [] devInfo;
// If we can't open a D2XX device, it's usually because the VCP drivers
// are installed, so it should also show up in the list of serial devices.
for (int i = 0; i < result.size(); ++i) {
CommPortPtr dev = result[i];
QString tmp;
if (dev->open(tmp))
dev->close();
else
result.remove(i--);
}
return result;
}

51
src/D2XX.h Normal file
View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2008 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_PT_D2XX_h
#define _GC_PT_D2XX_h 1
#include "CommPort.h"
#ifdef WIN32
#include <windows.h>
#endif
#include <ftd2xx.h>
class D2XX : public CommPort
{
D2XX(const D2XX &);
D2XX& operator=(const D2XX &);
FT_DEVICE_LIST_INFO_NODE info;
FT_HANDLE ftHandle;
bool isOpen;
D2XX(const FT_DEVICE_LIST_INFO_NODE &info);
public:
static QVector<CommPortPtr> myListCommPorts(QString &err);
virtual ~D2XX();
virtual bool open(QString &err);
virtual void close();
virtual int read(void *buf, size_t nbyte, QString &err);
virtual int write(void *buf, size_t nbyte, QString &err);
virtual QString name() const;
};
#endif // _GC_PT_D2XX_h

284
src/DBAccess.cpp Normal file
View File

@@ -0,0 +1,284 @@
/*
* Copyright (c) 2006 Justin Knotzke (jknotzke@shampoo.ca)
*
* 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 "DBAccess.h"
#include <QtSql>
#include <QtGui>
#include "RideFile.h"
#include "Zones.h"
#include "Settings.h"
#include "RideItem.h"
#include "RideMetric.h"
#include "TimeUtils.h"
#include <assert.h>
#include <math.h>
#include <QtXml/QtXml>
#include "SummaryMetrics.h"
DBAccess::DBAccess(QDir home)
{
initDatabase(home);
}
void DBAccess::closeConnection()
{
db.close();
}
QSqlDatabase DBAccess::initDatabase(QDir home)
{
if(db.isOpen())
return db;
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(home.absolutePath() + "/metricDB");
if (!db.open()) {
QMessageBox::critical(0, qApp->tr("Cannot open database"),
qApp->tr("Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"the Qt SQL driver documentation for information how "
"to build it.\n\n"
"Click Cancel to exit."), QMessageBox::Cancel);
return db;
}
return db;
}
bool DBAccess::createMetricsTable()
{
QSqlQuery query;
bool rc = query.exec("create table metrics (id integer primary key autoincrement, "
"filename varchar,"
"ride_date date,"
"ride_time double, "
"average_cad double,"
"workout_time double, "
"total_distance double,"
"x_power double,"
"average_speed double,"
"total_work double,"
"average_power double,"
"average_hr double,"
"relative_intensity double,"
"bike_score double)");
if(!rc)
qDebug() << query.lastError();
return rc;
}
bool DBAccess::createSeasonsTable()
{
QSqlQuery query;
bool rc = query.exec("CREATE TABLE seasons(id integer primary key autoincrement,"
"start_date date,"
"end_date date,"
"name varchar)");
if(!rc)
qDebug() << query.lastError();
return rc;
}
bool DBAccess::createDatabase()
{
bool rc = false;
rc = createMetricsTable();
if(!rc)
return rc;
rc = createIndex();
if(!rc)
return rc;
//Check to see if the table already exists..
QStringList tableList = db.tables(QSql::Tables);
if(!tableList.contains("seasons"))
return createSeasonsTable();
return true;
}
bool DBAccess::createIndex()
{
QSqlQuery query;
query.prepare("create INDEX IDX_FILENAME on metrics(filename)");
bool rc = query.exec();
if(!rc)
qDebug() << query.lastError();
return rc;
}
bool DBAccess::importRide(SummaryMetrics *summaryMetrics )
{
QSqlQuery query;
query.prepare("insert into metrics (filename, ride_date, ride_time, average_cad, workout_time, total_distance,"
"x_power, average_speed, total_work, average_power, average_hr,"
"relative_intensity, bike_score) values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
query.addBindValue(summaryMetrics->getFileName());
query.addBindValue(summaryMetrics->getRideDate());
query.addBindValue(summaryMetrics->getRideTime());
query.addBindValue(summaryMetrics->getCadence());
query.addBindValue(summaryMetrics->getWorkoutTime());
query.addBindValue(summaryMetrics->getDistance());
query.addBindValue(summaryMetrics->getXPower());
query.addBindValue(summaryMetrics->getSpeed());
query.addBindValue(summaryMetrics->getTotalWork());
query.addBindValue(summaryMetrics->getWatts());
query.addBindValue(summaryMetrics->getHeartRate());
query.addBindValue(summaryMetrics->getRelativeIntensity());
query.addBindValue(summaryMetrics->getBikeScore());
bool rc = query.exec();
if(!rc)
{
qDebug() << query.lastError();
}
return rc;
}
QStringList DBAccess::getAllFileNames()
{
QSqlQuery query("SELECT filename from metrics");
QStringList fileList;
while(query.next())
{
QString filename = query.value(0).toString();
fileList << filename;
}
return fileList;
}
QList<QDateTime> DBAccess::getAllDates()
{
QSqlQuery query("SELECT ride_date from metrics");
QList<QDateTime> dates;
while(query.next())
{
QDateTime date = query.value(0).toDateTime();
dates << date;
}
return dates;
}
QList<SummaryMetrics> DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
{
QList<SummaryMetrics> metrics;
QSqlQuery query("SELECT filename, ride_date, ride_time, average_cad, workout_time, total_distance,"
"x_power, average_speed, total_work, average_power, average_hr,"
"relative_intensity, bike_scoreFROM metrics WHERE ride_date >=:start AND ride_date <=:end");
query.bindValue(":start", start);
query.bindValue(":end", end);
while(query.next())
{
SummaryMetrics summaryMetrics;
summaryMetrics.setFileName(query.value(0).toString());
summaryMetrics.setRideDate(query.value(1).toDateTime());
summaryMetrics.setRideTime(query.value(2).toDouble());
summaryMetrics.setCadence(query.value(3).toDouble());
summaryMetrics.setWorkoutTime(query.value(4).toDouble());
summaryMetrics.setDistance(query.value(5).toDouble());
summaryMetrics.setXPower(query.value(6).toDouble());
summaryMetrics.setSpeed(query.value(7).toDouble());
summaryMetrics.setTotalWork(query.value(8).toDouble());
summaryMetrics.setWatts(query.value(9).toDouble());
summaryMetrics.setHeartRate(query.value(10).toDouble());
summaryMetrics.setRelativeIntensity(query.value(11).toDouble());
summaryMetrics.setBikeScore(query.value(12).toDouble());
metrics << summaryMetrics;
}
return metrics;
}
bool DBAccess::createSeason(Season season)
{
QSqlQuery query;
query.prepare("INSERT INTO season (start_date, end_date, name) values (?,?,?)");
query.addBindValue(season.getStart());
query.addBindValue(season.getEnd());
query.addBindValue(season.getName());
bool rc = query.exec();
if(!rc)
qDebug() << query.lastError();
return rc;
}
QList<Season> DBAccess::getAllSeasons()
{
QSqlQuery query("SELECT start_date, end_date, name from season");
QList<Season> seasons;
while(query.next())
{
Season season;
season.setStart(query.value(0).toDateTime());
season.setEnd(query.value(1).toDateTime());
season.setName(query.value(2).toString());
seasons << season;
}
return seasons;
}
bool DBAccess::dropMetricTable()
{
QStringList tableList = db.tables(QSql::Tables);
if(!tableList.contains("metrics"))
return true;
QSqlQuery query("DROP TABLE metrics");
return query.exec();
}

61
src/DBAccess.h Normal file
View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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_DBAccess_h
#define _GC_DBAccess_h 1
#import <QDir>
#import <QHash>
#import <QtSql>
#import "SummaryMetrics.h"
#import "Season.h"
class RideFile;
class Zones;
class RideMetric;
class DBAccess
{
public:
DBAccess(QDir home);
typedef QHash<QString,RideMetric*> MetricMap;
void importAllRides(QDir path, Zones *zones);
bool importRide(SummaryMetrics *summaryMetrics);
bool createDatabase();
QStringList getAllFileNames();
void closeConnection();
QList<QDateTime> getAllDates();
QList<SummaryMetrics> getAllMetricsFor(QDateTime start, QDateTime end);
bool createSeasonsTable();
bool createMetricsTable();
bool createSeason(Season season);
QList<Season> getAllSeasons();
bool dropMetricTable();
//bool deleteSeason(Season season);
private:
QSqlDatabase db;
bool createIndex();
QSqlDatabase initDatabase(QDir home);
};
#endif

140
src/DatePickerDialog.cpp Normal file
View File

@@ -0,0 +1,140 @@
/*
* Copyright (c) 2007 Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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 "DatePickerDialog.h"
#include "Settings.h"
#include <QtGui>
void DatePickerDialog::setupUi(QDialog *DatePickerDialog)
{
if (DatePickerDialog->objectName().isEmpty())
DatePickerDialog->setObjectName(QString::fromUtf8("DatePickerDialog"));
DatePickerDialog->setWindowModality(Qt::WindowModal);
DatePickerDialog->setAcceptDrops(false);
DatePickerDialog->setModal(true);
QGridLayout *mainGrid = new QGridLayout(this); // a 2 x n grid
lblOccur = new QLabel("When did this ride occur?", this);
mainGrid->addWidget(lblOccur, 0,0);
dateTimeEdit = new QDateTimeEdit(this);
// preset dialog to today's date -thm
QDateTime *dt = new QDateTime;
date = dt->currentDateTime();
dateTimeEdit->setDateTime(date);
mainGrid->addWidget(dateTimeEdit,0,1);
lblBrowse = new QLabel("Choose a CSV file to upload", this);
mainGrid->addWidget(lblBrowse, 1,0);
btnBrowse = new QPushButton(this);
mainGrid->addWidget(btnBrowse,2,0);
txtBrowse = new QLineEdit(this);
mainGrid->addWidget(txtBrowse,2,1);
btnOK = new QPushButton(this);
mainGrid->addWidget(btnOK, 3,0);
btnCancel = new QPushButton(this);
mainGrid->addWidget(btnCancel, 3,1);
DatePickerDialog->setWindowTitle(
QApplication::translate("DatePickerDialog", "Import CSV file", 0,
QApplication::UnicodeUTF8));
btnBrowse->setText(
QApplication::translate("DatePickerDialog", "File to import...", 0,
QApplication::UnicodeUTF8));
btnOK->setText(
QApplication::translate("DatePickerDialog", "OK", 0,
QApplication::UnicodeUTF8));
btnCancel->setText(
QApplication::translate("DatePickerDialog", "Cancel", 0,
QApplication::UnicodeUTF8));
connect(btnOK, SIGNAL(clicked()), this, SLOT(on_btnOK_clicked()));
connect(btnBrowse, SIGNAL(clicked()), this, SLOT(on_btnBrowse_clicked()));
connect(btnCancel, SIGNAL(clicked()), this, SLOT(on_btnCancel_clicked()));
// disable date picker and OK button until a file has been selected
dateTimeEdit->setEnabled(FALSE);
lblOccur->setEnabled(FALSE);
btnOK->setEnabled(FALSE);
Q_UNUSED(DatePickerDialog);
}
DatePickerDialog::DatePickerDialog(QWidget *parent)
: QDialog(parent)
{
setupUi(this);
}
void DatePickerDialog::on_btnOK_clicked()
{
canceled = false;
date = dateTimeEdit->dateTime();
accept();
}
void DatePickerDialog::on_btnBrowse_clicked()
{
//First check to see if the Library folder exists where the executable is (for USB sticks)
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant lastDirVar = settings->value(GC_SETTINGS_LAST_IMPORT_PATH);
QString lastDir = (lastDirVar != QVariant())
? lastDirVar.toString() : QDir::homePath();
fileName = QFileDialog::getOpenFileName(
this, tr("Import CSV"), lastDir,
tr("Comma Separated Values (*.csv)"));
if (!fileName.isEmpty()) {
lastDir = QFileInfo(fileName).absolutePath();
settings->setValue(GC_SETTINGS_LAST_IMPORT_PATH, lastDir);
// Find the datetimestamp from the filename.
// If we can't, use the creation time.
// eg. GoldenCheetah YYYY_MM_DD_HH_MM_SS.csv
// Ergomo YYYYMMDD_HHMMSS_NAME_SURNAME.CSV
QFileInfo *qfi = new QFileInfo(fileName);
QString name = qfi->baseName();
QRegExp rxGoldenCheetah("^(19|20)\\d\\d_[01]\\d_[0123]\\d_[012]\\d_[012345]\\d_[012345]\\d$");
QRegExp rxErgomo("^(19|20)\\d\\d[01]\\d[0123]\\d_[012]\\d[012345]\\d[012345]\\d_[A-Z_]+$");
if (rxGoldenCheetah.indexIn(name) == 0) {
date = QDateTime::fromString(name.left(19), "yyyy_MM_dd_hh_mm_ss");
} else if (rxErgomo.indexIn(name) == 0) {
date = QDateTime::fromString(name.left(15), "yyyyMMdd_hhmmss");
} else {
date = qfi->created();
}
// and put it into the datePicker dialog
dateTimeEdit->setDateTime(date);
}
txtBrowse->setText(fileName);
// allow date to be changed, and enable OK button
dateTimeEdit->setEnabled(TRUE);
lblOccur->setEnabled(TRUE);
btnOK->setEnabled(TRUE);
}
void DatePickerDialog::on_btnCancel_clicked()
{
canceled = true;
reject();
}

48
src/DatePickerDialog.h Normal file
View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2007 Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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 <QDateTime>
#include <QtGui>
class DatePickerDialog : public QDialog
{
Q_OBJECT
public:
DatePickerDialog(QWidget *parent = 0);
QString fileName;
QDateTime date;
bool canceled;
QString currentText;
QLabel *lblOccur;
QDateTimeEdit *dateTimeEdit;
QHBoxLayout *hboxLayout1;
QLabel *lblImport;
QLabel *lblBrowse;
QPushButton *btnBrowse;
QLineEdit *txtBrowse;
QPushButton *btnOK;
QPushButton *btnCancel;
void setupUi(QDialog*);
private slots:
void on_btnOK_clicked();
void on_btnBrowse_clicked();
void on_btnCancel_clicked();
};

62
src/DaysScaleDraw.h Normal file
View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2009 Robert Carlsen (robert@robertcarlsen.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
*
* DayScaleDraw.h
* GoldenCheetah
*
* Provides specialized formatting for Plot axes
*/
#include <qwt_scale_draw.h>
class DaysScaleDraw: public QwtScaleDraw
{
public:
DaysScaleDraw()
{
}
virtual QwtText label(double v) const
{
switch(int(v))
{
case 1:
return QString("Mon");
break;
case 2:
return QString("Tue");
break;
case 3:
return QString("Wed");
break;
case 4:
return QString("Thu");
break;
case 5:
return QString("Fri");
break;
case 6:
return QString("Sat");
break;
case 7:
return QString("Sun");
break;
default:
return QString(int(v));
break;
}
}
};

53
src/Device.cpp Normal file
View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2008 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 "Device.h"
typedef QMap<QString,Device*> DevicesMap;
static DevicesMap *devicesPtr;
inline DevicesMap &
devices()
{
if (devicesPtr == NULL)
devicesPtr = new QMap<QString,Device*>;
return *devicesPtr;
}
QList<QString>
Device::deviceTypes()
{
return devices().keys();
}
Device &
Device::device(const QString &deviceType)
{
assert(devices().contains(deviceType));
return *devices().value(deviceType);
}
bool
Device::addDevice(const QString &deviceType, Device *device)
{
assert(!devices().contains(deviceType));
devices().insert(deviceType, device);
return true;
}

43
src/Device.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2008 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_Device_h
#define _GC_Device_h 1
#include "CommPort.h"
#include <boost/function.hpp>
struct Device
{
virtual ~Device() {}
typedef boost::function<bool (const QString &statusText)> StatusCallback;
virtual QString downloadInstructions() const = 0;
virtual bool download(CommPortPtr dev, const QDir &tmpdir,
QString &tmpname, QString &filename,
StatusCallback statusCallback, QString &err) = 0;
virtual void cleanup(CommPortPtr dev) { (void) dev; }
static QList<QString> deviceTypes();
static Device &device(const QString &deviceType);
static bool addDevice(const QString &deviceType, Device *device);
};
#endif // _GC_Device_h

213
src/DownloadRideDialog.cpp Normal file
View File

@@ -0,0 +1,213 @@
/*
* $Id: DownloadRideDialog.cpp,v 1.4 2006/08/11 20:02:13 srhea Exp $
*
* Copyright (c) 2006-2008 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 "Device.h"
#include "MainWindow.h"
#include <assert.h>
#include <errno.h>
#include <QtGui>
#include <boost/bind.hpp>
#include <boost/foreach.hpp>
DownloadRideDialog::DownloadRideDialog(MainWindow *mainWindow,
const QDir &home) :
mainWindow(mainWindow), home(home), cancelled(false),
downloadInProgress(false)
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle("Download Ride Data");
portCombo = new QComboBox(this);
QLabel *instructLabel = new QLabel(tr("Instructions:"), this);
label = new QLabel(this);
label->setIndent(10);
deviceCombo = new QComboBox(this);
QList<QString> deviceTypes = Device::deviceTypes();
assert(deviceTypes.size() > 0);
BOOST_FOREACH(QString device, deviceTypes) {
deviceCombo->addItem(device);
}
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(scanCommPorts()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(downloadButton);
buttonLayout->addWidget(rescanButton);
buttonLayout->addWidget(cancelButton);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(new QLabel(tr("Select port:"), this));
mainLayout->addWidget(portCombo);
mainLayout->addWidget(new QLabel(tr("Select device type:"), this));
mainLayout->addWidget(deviceCombo);
mainLayout->addWidget(instructLabel);
mainLayout->addWidget(label);
mainLayout->addLayout(buttonLayout);
scanCommPorts();
}
void
DownloadRideDialog::setReadyInstruct()
{
if (portCombo->count() == 0) {
label->setText(tr("No devices found. Make sure the device\n"
"unit is plugged into the computer,\n"
"then click \"Rescan\" to check again."));
downloadButton->setEnabled(false);
}
else {
Device &device = Device::device(deviceCombo->currentText());
QString inst = device.downloadInstructions();
if (inst.size() == 0)
label->setText("Click Download to begin downloading.");
else
label->setText(inst + ", \nthen click Download.");
downloadButton->setEnabled(true);
}
}
void
DownloadRideDialog::scanCommPorts()
{
portCombo->clear();
QString err;
devList = CommPort::listCommPorts(err);
if (err != "") {
QString msg = "Warning:\n\n" + err + "You may need to (re)install "
"the FTDI drivers before downloading.";
QMessageBox::warning(0, "Error Loading Device Drivers", msg,
QMessageBox::Ok, QMessageBox::NoButton);
}
for (int i = 0; i < devList.size(); ++i)
portCombo->addItem(devList[i]->name());
if (portCombo->count() > 0)
downloadButton->setFocus();
setReadyInstruct();
}
bool
DownloadRideDialog::statusCallback(const QString &statusText)
{
label->setText(statusText);
QCoreApplication::processEvents();
return !cancelled;
}
void
DownloadRideDialog::downloadClicked()
{
downloadButton->setEnabled(false);
rescanButton->setEnabled(false);
downloadInProgress = true;
CommPortPtr dev;
for (int i = 0; i < devList.size(); ++i) {
if (devList[i]->name() == portCombo->currentText()) {
dev = devList[i];
break;
}
}
assert(dev);
QString err;
QString tmpname, filename;
Device &device = Device::device(deviceCombo->currentText());
if (!device.download(
dev, home, tmpname, filename,
boost::bind(&DownloadRideDialog::statusCallback, this, _1), err))
{
if (cancelled) {
QMessageBox::information(this, tr("Download canceled"),
tr("Cancel clicked by user."));
cancelled = false;
}
else {
QMessageBox::information(this, tr("Download failed"), err);
}
downloadInProgress = false;
reject();
return;
}
QString filepath = home.absolutePath() + "/" + filename;
if (QFile::exists(filepath)) {
if (QMessageBox::warning(
this,
tr("Ride Already Downloaded"),
tr("This ride appears to have already ")
+ tr("been downloaded. Do you want to ")
+ tr("overwrite the previous download?"),
tr("&Overwrite"), tr("&Cancel"),
QString(), 1, 1) == 1) {
reject();
return;
}
}
#ifdef __WIN32__
// Windows ::rename won't overwrite an existing file.
if (QFile::exists(filepath)) {
QFile old(filepath);
if (!old.remove()) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to remove existing file ")
+ filepath + ": " + old.error());
QFile::remove(tmpname);
reject();
}
}
#endif
// Use ::rename() instead of QFile::rename() to get forced overwrite.
if (rename(QFile::encodeName(tmpname), QFile::encodeName(filepath)) < 0) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to rename ") + tmpname + tr(" to ")
+ filepath + ": " + strerror(errno));
QFile::remove(tmpname);
reject();
return;
}
QMessageBox::information(this, tr("Success"), tr("Download complete."));
mainWindow->addRide(filename);
device.cleanup(dev);
downloadInProgress = false;
accept();
}
void
DownloadRideDialog::cancelClicked()
{
if (!downloadInProgress)
reject();
else
cancelled = true;
}

View File

@@ -1,7 +1,5 @@
/*
* $Id: DownloadRideDialog.h,v 1.4 2006/08/11 20:02:13 srhea Exp $
*
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
* Copyright (c) 2006-2008 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
@@ -21,10 +19,8 @@
#ifndef _GC_DownloadRideDialog_h
#define _GC_DownloadRideDialog_h 1
#include "CommPort.h"
#include <QtGui>
extern "C" {
#include "pt.h"
}
class MainWindow;
@@ -34,39 +30,26 @@ class DownloadRideDialog : public QDialog
public:
DownloadRideDialog(MainWindow *mainWindow, const QDir &home);
~DownloadRideDialog();
void time_cb(struct tm *time);
void record_cb(unsigned char *buf);
void downloadFinished();
bool statusCallback(const QString &statusText);
private slots:
void downloadClicked();
void cancelClicked();
void setReadyInstruct();
void scanDevices();
void readVersion();
void readData();
void versionTimeout();
void scanCommPorts();
private:
MainWindow *mainWindow;
QDir home;
QListWidget *listWidget;
QPushButton *downloadButton, *rescanButton, *cancelButton;
QComboBox *portCombo, *deviceCombo;
QLabel *label;
int endingOffset;
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;
QVector<CommPortPtr> devList;
bool cancelled, downloadInProgress;
};
#endif // _GC_DownloadRideDialog_h

View File

@@ -1,6 +1,4 @@
/*
* $Id: LogTimeScaleDraw.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
@@ -23,7 +21,6 @@
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the Qwt License, Version 1.0
*
*/
#include "LogTimeScaleDraw.h"
@@ -98,8 +95,8 @@ LogTimeScaleDraw::tickLabel(const QFont &font, double value) const
else
lbl = label(value);
lbl.setFlags(0);
lbl.setLayoutAttributes(QwtText::MinimumLayout);
lbl.setRenderFlags(0);
lbl.setLayoutAttribute(QwtText::MinimumLayout);
(void)lbl.textSize(font); // initialize the internal cache

View File

@@ -1,6 +1,4 @@
/*
* $Id: LogTimeScaleDraw.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
@@ -23,7 +21,6 @@
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the Qwt License, Version 1.0
*
*/
#ifndef _GC_LogTimeScaleDraw_h

View File

@@ -1,6 +1,4 @@
/*
* $Id: LogTimeScaleEngine.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
@@ -23,7 +21,6 @@
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the Qwt License, Version 1.0
*
*/
#include "LogTimeScaleEngine.h"
@@ -34,10 +31,10 @@
/*!
Return a transformation, for logarithmic (base 10) scales
*/
QwtScaleTransformation LogTimeScaleEngine::transformation() const
QwtScaleTransformation *LogTimeScaleEngine::transformation() const
{
return QwtScaleTransformation(QwtScaleTransformation::log10XForm,
QwtScaleTransformation::log10InvXForm);
return new QwtScaleTransformation(QwtScaleTransformation::Log10);
//, log10XForm, QwtScaleTransformation::log10InvXForm);
}
/*!
@@ -56,8 +53,16 @@ void LogTimeScaleEngine::autoScale(int maxNumSteps,
if ( x1 > x2 )
qSwap(x1, x2);
QwtDoubleInterval interval(x1 / pow(10.0, loMargin()),
x2 * pow(10.0, hiMargin()) );
QwtDoubleInterval interval(
#if (QWT_VERSION >= 0x050200)
x1 / pow(10.0, lowerMargin()),
x2 * pow(10.0, upperMargin())
#else
x1 / pow(10.0, loMargin()),
x2 * pow(10.0, hiMargin())
#endif
);
double logRef = 1.0;
if (reference() > LOG_MIN / 2)
@@ -73,7 +78,7 @@ void LogTimeScaleEngine::autoScale(int maxNumSteps,
if (testAttribute(QwtScaleEngine::IncludeReference))
interval = interval.extend(logRef);
interval = interval.limit(LOG_MIN, LOG_MAX);
interval = interval.limited(LOG_MIN, LOG_MAX);
if (interval.width() == 0.0)
interval = buildInterval(interval.minValue());
@@ -111,7 +116,7 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
int maxMajSteps, int maxMinSteps, double stepSize) const
{
QwtDoubleInterval interval = QwtDoubleInterval(x1, x2).normalized();
interval = interval.limit(LOG_MIN, LOG_MAX);
interval = interval.limited(LOG_MIN, LOG_MAX);
if (interval.width() <= 0 )
return QwtScaleDiv();
@@ -123,7 +128,13 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
QwtLinearScaleEngine linearScaler;
linearScaler.setAttributes(attributes());
linearScaler.setReference(reference());
linearScaler.setMargins(loMargin(), hiMargin());
linearScaler.setMargins(
#if (QWT_VERSION >= 0x050200)
lowerMargin(), upperMargin()
#else
loMargin(), hiMargin()
#endif
);
return linearScaler.divideScale(x1, x2,
maxMajSteps, maxMinSteps, stepSize);
@@ -143,7 +154,7 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
QwtScaleDiv scaleDiv;
if ( stepSize != 0.0 )
{
QwtTickList ticks[QwtScaleDiv::NTickTypes];
QwtValueList ticks[QwtScaleDiv::NTickTypes];
buildTicks(interval, stepSize, maxMinSteps, ticks);
scaleDiv = QwtScaleDiv(interval, ticks);
@@ -157,7 +168,7 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
void LogTimeScaleEngine::buildTicks(
const QwtDoubleInterval& interval, double stepSize, int maxMinSteps,
QwtTickList ticks[QwtScaleDiv::NTickTypes]) const
QwtValueList ticks[QwtScaleDiv::NTickTypes]) const
{
const QwtDoubleInterval boundingInterval =
align(interval, stepSize);
@@ -181,7 +192,7 @@ struct tick_info_t {
};
tick_info_t tick_info[] = {
{ 0.021, "1.26s" },
{ 1.0/60.0, "1s" },
{ 5.0/60.0, "5s" },
{ 15.0/60.0, "15s" },
{ 0.5, "30s" },
@@ -199,10 +210,12 @@ tick_info_t tick_info[] = {
{ -1.0, NULL }
};
QwtTickList LogTimeScaleEngine::buildMajorTicks(
QwtValueList LogTimeScaleEngine::buildMajorTicks(
const QwtDoubleInterval &interval, double stepSize) const
{
QwtTickList ticks;
(void) interval;
(void) stepSize;
QwtValueList ticks;
tick_info_t *walker = tick_info;
while (walker->label) {
ticks += walker->x;
@@ -211,11 +224,14 @@ QwtTickList LogTimeScaleEngine::buildMajorTicks(
return ticks;
}
QwtTickList LogTimeScaleEngine::buildMinorTicks(
const QwtTickList &majorTicks,
QwtValueList LogTimeScaleEngine::buildMinorTicks(
const QwtValueList &majorTicks,
int maxMinSteps, double stepSize) const
{
QwtTickList minorTicks;
(void) majorTicks;
(void) maxMinSteps;
(void) stepSize;
QwtValueList minorTicks;
return minorTicks;
}

View File

@@ -1,6 +1,4 @@
/*
* $Id: LogTimeScaleEngine.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
@@ -23,7 +21,6 @@
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the Qwt License, Version 1.0
*
*/
#ifndef _GC_LogTimeScaleEngine_h
@@ -41,7 +38,7 @@ class LogTimeScaleEngine : public QwtScaleEngine
int numMajorSteps, int numMinorSteps,
double stepSize = 0.0) const;
virtual QwtScaleTransformation transformation() const;
virtual QwtScaleTransformation *transformation() const;
protected:
QwtDoubleInterval log10(const QwtDoubleInterval&) const;
@@ -53,13 +50,13 @@ class LogTimeScaleEngine : public QwtScaleEngine
void buildTicks(
const QwtDoubleInterval &, double stepSize, int maxMinSteps,
QwtTickList ticks[QwtScaleDiv::NTickTypes]) const;
QwtValueList ticks[QwtScaleDiv::NTickTypes]) const;
QwtTickList buildMinorTicks(
const QwtTickList& majorTicks,
QwtValueList buildMinorTicks(
const QwtValueList& majorTicks,
int maxMinMark, double step) const;
QwtTickList buildMajorTicks(
QwtValueList buildMajorTicks(
const QwtDoubleInterval &interval, double stepSize) const;
};

2215
src/MainWindow.cpp Normal file

File diff suppressed because it is too large Load Diff

183
src/MainWindow.h Normal file
View File

@@ -0,0 +1,183 @@
/*
* 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 <QDir>
#include <QtGui>
#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include "RideItem.h"
#include <boost/shared_ptr.hpp>
class AllPlot;
class CpintPlot;
class PfPvPlot;
class PowerHist;
class QwtPlotPanner;
class QwtPlotPicker;
class QwtPlotZoomer;
class RideFile;
class Zones;
class RideCalendar;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(const QDir &home);
void addRide(QString name, bool bSelect=true);
void removeCurrentRide();
const RideFile *currentRide();
void getBSFactors(float &timeBS, float &distanceBS);
QDir home;
protected:
virtual void resizeEvent(QResizeEvent*);
virtual void moveEvent(QMoveEvent*);
virtual void closeEvent(QCloseEvent*);
private slots:
void rideSelected();
void leftLayoutMoved();
void splitterMoved();
void newCyclist();
void openCyclist();
void downloadRide();
void manualRide();
void exportCSV();
void exportXML();
void importCSV();
void importSRM();
void importTCX();
void importWKO();
void importPolar();
void importQuarq();
void findBestIntervals();
void splitRide();
void deleteRide();
void setAllPlotWidgets(RideItem *rideItem);
void setHistWidgets(RideItem *rideItem);
void setSmoothingFromSlider();
void setSmoothingFromLineEdit();
void cpintSetCPButtonClicked();
void setBinWidthFromSlider();
void setBinWidthFromLineEdit();
void setlnYHistFromCheckBox();
void setWithZerosFromCheckBox();
void setHistSelection(int id);
void setQaCPFromLineEdit();
void setQaCADFromLineEdit();
void setQaCLFromLineEdit();
void setShadeZonesPfPvFromCheckBox();
void tabChanged(int index);
void pickerMoved(const QPoint &);
void aboutDialog();
void notesChanged();
void saveNotes();
void showOptions();
void showTools();
void importRideToDB();
void scanForMissing();
void generateWeeklySummary();
void dateChanged(const QDate &);
protected:
static QString notesFileName(QString rideFileName);
private:
bool parseRideFileName(const QString &name, QString *notesFileName, QDateTime *dt);
void setHistBinWidthText();
void setHistTextValidator();
boost::shared_ptr<QSettings> settings;
RideCalendar *calendar;
QSplitter *splitter;
QTreeWidget *treeWidget;
QTabWidget *tabWidget;
QTextEdit *rideSummary;
QTextEdit *weeklySummary;
AllPlot *allPlot;
QwtPlotZoomer *allZoomer;
QwtPlotPanner *allPanner;
QCheckBox *showHr;
QCheckBox *showSpeed;
QCheckBox *showCad;
QCheckBox *showAlt;
QComboBox *showPower;
CpintPlot *cpintPlot;
QLineEdit *cpintTimeValue;
QLineEdit *cpintTodayValue;
QLineEdit *cpintAllValue;
QPushButton *cpintSetCPButton;
QwtPlotPicker *picker;
QSlider *smoothSlider;
QLineEdit *smoothLineEdit;
QSlider *binWidthSlider;
QLineEdit *binWidthLineEdit;
QCheckBox *lnYHistCheckBox;
QCheckBox *withZerosCheckBox;
QComboBox *histParameterCombo;
QCheckBox *shadeZonesPfPvCheckBox;
QTreeWidgetItem *allRides;
PowerHist *powerHist;
QwtPlot *weeklyPlot;
QwtPlotCurve *weeklyDistCurve;
QwtPlotCurve *weeklyDurationCurve;
QwtPlotCurve *weeklyBaselineCurve;
QwtPlotCurve *weeklyBSBaselineCurve;
QwtPlot *weeklyBSPlot;
QSplitter *leftLayout;
QwtPlotCurve *weeklyBSCurve;
QwtPlotCurve *weeklyRICurve;
Zones *zones;
// pedal force/pedal velocity scatter plot widgets
PfPvPlot *pfPvPlot;
QLineEdit *qaCPValue;
QLineEdit *qaCadValue;
QLineEdit *qaClValue;
QTextEdit *rideNotes;
QString currentNotesFile;
bool currentNotesChanged;
RideItem *ride; // the currently selected ride
int histWattsShadedID;
int histWattsUnshadedID;
int histNmID;
int histHrID;
int histKphID;
int histCadID;
int histAltID;
bool useMetricUnits; // whether metric units are used (or imperial)
float timebsfactor;
float distancebsfactor;
};
#endif // _GC_MainWindow_h

View File

@@ -1,34 +0,0 @@
#
# $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
#
SUBDIRS=lib cmd gui # order is important!
all: gui-makefile subdirs
.PHONY: all gui-makefile subdirs clean
gui-makefile:
cd gui; qmake GoldenCheetah.pro
clean:
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir clean; done
subdirs:
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir; done

372
src/ManualRideDialog.cpp Normal file
View File

@@ -0,0 +1,372 @@
/*
* $Id:$
*
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
*
* 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 "ManualRideDialog.h"
#include "MainWindow.h"
#include "Settings.h"
#include <assert.h>
#include <string.h>
#include <errno.h>
#include <QtGui>
#include <math.h>
#include <boost/bind.hpp>
ManualRideDialog::ManualRideDialog(MainWindow *mainWindow,
const QDir &home, bool useMetric) :
mainWindow(mainWindow), home(home)
{
useMetricUnits = useMetric;
int row;
mainWindow->getBSFactors(timeBS,distanceBS);
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Manually Enter Ride Data"));
// ride date
QLabel *manualDateLabel = new QLabel(tr("Ride date: "), this);
dateTimeEdit = new QDateTimeEdit( QDateTime::currentDateTime(), this );
// Wed 6/24/09 6:55 AM
dateTimeEdit->setDisplayFormat(tr("ddd MMM d, yyyy h:mm AP"));
// ride length
QLabel *manualLengthLabel = new QLabel(tr("Ride length: "), this);
QHBoxLayout *manualLengthLayout = new QHBoxLayout;
hrslbl = new QLabel(tr("hours"),this);
hrslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken);
hrsentry = new QLineEdit(this);
QIntValidator * hoursValidator = new QIntValidator(0,99,this);
//hrsentry->setInputMask("09");
hrsentry->setValidator(hoursValidator);
manualLengthLayout->addWidget(hrslbl);
manualLengthLayout->addWidget(hrsentry);
minslbl = new QLabel(tr("mins"),this);
minslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken);
minsentry = new QLineEdit(this);
QIntValidator * secsValidator = new QIntValidator(0,60,this);
//minsentry->setInputMask("00");
minsentry->setValidator(secsValidator);
manualLengthLayout->addWidget(minslbl);
manualLengthLayout->addWidget(minsentry);
secslbl = new QLabel(tr("secs"),this);
secslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken);
secsentry = new QLineEdit(this);
//secsentry->setInputMask("00");
secsentry->setValidator(secsValidator);
manualLengthLayout->addWidget(secslbl);
manualLengthLayout->addWidget(secsentry);
// ride distance
QString *DistanceString = new QString(tr("Distance "));
if (useMetricUnits)
DistanceString->append("(" + tr("km") + "):");
else
DistanceString->append("(" + tr("miles") + "):");
QLabel *DistanceLabel = new QLabel(*DistanceString, this);
QDoubleValidator * distanceValidator = new QDoubleValidator(0,1000,2,this);
distanceentry = new QLineEdit(this);
//distanceentry->setInputMask("009.00");
distanceentry->setValidator(distanceValidator);
// AvgHR
QLabel *HRLabel = new QLabel(tr("Average HR: "), this);
HRentry = new QLineEdit(this);
QIntValidator *hrValidator = new QIntValidator(0,200,this);
//HRentry->setInputMask("099");
HRentry->setValidator(hrValidator);
// how to estimate BikeScore:
QLabel *BSEstLabel;
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant BSmode = settings->value(GC_BIKESCOREMODE);
estBSbyTimeButton = NULL;
estBSbyDistButton = NULL;
if (timeBS || distanceBS) {
BSEstLabel = new QLabel(tr("Estimate BikeScore by: "));
if (timeBS) {
estBSbyTimeButton = new QRadioButton(tr("Time"));
// default to time based unless no timeBS factor
if (BSmode.toString() != "distance")
estBSbyTimeButton->setDown(true);
}
if (distanceBS) {
estBSbyDistButton = new QRadioButton(tr("Distance"));
if (BSmode.toString() == "distance" || ! timeBS)
estBSbyDistButton->setDown(true);
}
}
// BikeScore
QLabel *ManualBSLabel = new QLabel(tr("BikeScore: "), this);
BSentry = new QLineEdit(this);
BSentry->setInputMask("009");
BSentry->clear();
// buttons
enterButton = new QPushButton(tr("&OK"), this);
cancelButton = new QPushButton(tr("&Cancel"), this);
// don't let Enter write a new (and possibly incomplete) manual file:
enterButton->setDefault(false);
cancelButton->setDefault(true);
// Set up the layout:
QGridLayout *glayout = new QGridLayout(this);
row = 0;
glayout->addWidget(manualDateLabel, row, 0);
glayout->addWidget(dateTimeEdit, row, 1, 1, -1);
row++;
glayout->addWidget(manualLengthLabel, row, 0);
glayout->addLayout(manualLengthLayout,row,1,1,-1);
row++;
glayout->addWidget(DistanceLabel,row,0);
glayout->addWidget(distanceentry,row,1,1,-1);
row++;
glayout->addWidget(HRLabel,row,0);
glayout->addWidget(HRentry,row,1,1,-1);
row++;
if (timeBS || distanceBS) {
glayout->addWidget(BSEstLabel,row,0);
if (estBSbyTimeButton)
glayout->addWidget(estBSbyTimeButton,row,1,1,-1);
if (estBSbyDistButton)
glayout->addWidget(estBSbyDistButton,row,2,1,-1);
row++;
}
glayout->addWidget(ManualBSLabel,row,0);
glayout->addWidget(BSentry,row,1,1,-1);
row++;
glayout->addWidget(enterButton,row,1);
glayout->addWidget(cancelButton,row,2);
this->resize(QSize(400,275));
connect(enterButton, SIGNAL(clicked()), this, SLOT(enterClicked()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
connect(hrsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
//connect(secsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
connect(minsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
connect(distanceentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
if (estBSbyTimeButton)
connect(estBSbyTimeButton,SIGNAL(clicked()),this, SLOT(bsEstChanged()));
if (estBSbyDistButton)
connect(estBSbyDistButton,SIGNAL(clicked()),this, SLOT(bsEstChanged()));
}
void
ManualRideDialog::estBSFromDistance()
{
// calculate distance-based BS estimate
double bs = 0;
bs = distanceentry->text().toFloat() * distanceBS;
QString text = QString("%1").arg((int)bs);
// cast to int so QLineEdit doesn't interpret "51.3" as "513"
BSentry->clear();
BSentry->insert(text);
}
void
ManualRideDialog::estBSFromTime()
{
// calculate time-based BS estimate
double bs = 0;
bs = ((hrsentry->text().toInt() ) +
(minsentry->text().toInt() / 60) +
(secsentry->text().toInt()/ 3600)) * timeBS;
QString text = QString("%1").arg((int)bs);
BSentry->clear();
BSentry->insert(text);
}
void
ManualRideDialog::bsEstChanged()
{
if (estBSbyDistButton->isChecked()) {
estBSFromDistance();
}
else {
estBSFromTime();
}
}
void
ManualRideDialog::setBsEst()
{
if (estBSbyDistButton) {
if (estBSbyDistButton->isChecked()) {
estBSFromDistance();
}
else {
estBSFromTime();
}
}
}
void
ManualRideDialog::cancelClicked()
{
reject();
}
void
ManualRideDialog::enterClicked()
{
// write data to manual entry file
if (filename == "") {
char tmp[32];
// use user's time for file:
QDateTime lt = dateTimeEdit->dateTime().toLocalTime();
sprintf(tmp, "%04d_%02d_%02d_%02d_%02d_%02d.man",
lt.date().year(), lt.date().month(),
lt.date().day(), lt.time().hour(),
lt.time().minute(),
lt.time().second());
filename = tmp;
filepath = home.absolutePath() + "/" + filename;
FILE *out = fopen(filepath.toAscii().constData(), "r");
if (out) {
fclose(out);
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();
return ;
}
}
}
QString tmpname;
{
// QTemporaryFile doesn't actually close the file on .close(); it
// closes the file when in its destructor. On Windows, we can't
// rename an open file. So let tmp go out of scope before calling
// rename.
QString tmpl = home.absoluteFilePath(".ptdl.XXXXXX");
QTemporaryFile tmp(tmpl);
tmp.setAutoRemove(false);
if (!tmp.open()) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to create temporary file ")
+ tmpl + ": " + tmp.error());
reject();
return;
}
QTextStream out(&tmp);
tmpname = tmp.fileName(); // after close(), tmp.fileName() is ""
/*
* File format:
* "manual"
* "minutes,mph,watts,miles,hr,bikescore" # header (metric or imperial)
* minutes,mph,watts,miles,hr,bikeScore # data
*/
out << "manual\n";
if (useMetricUnits)
out << "minutes,kmh,watts,km,hr,bikescore\n";
else
out << "minutes,mph,watts,miles,hr,bikescore\n";
// data
double secs = (hrsentry->text().toInt() * 3600) +
(minsentry->text().toInt() * 60) +
(secsentry->text().toInt());
out << secs/60.0;
out << ",";
out << distanceentry->text().toFloat() / (secs / 3600.0);
out << ",";
out << 0.0; // watts
out << ",";
out << distanceentry->text().toFloat();
out << ",";
out << HRentry->text().toInt();
out << ",";
out << BSentry->text().toInt();
out << "\n";
tmp.close();
// QTemporaryFile initially has permissions set to 0600.
// Make it readable by everyone.
tmp.setPermissions(tmp.permissions()
| QFile::ReadOwner | QFile::ReadUser
| QFile::ReadGroup | QFile::ReadOther);
}
#ifdef __WIN32__
// Windows ::rename won't overwrite an existing file.
if (QFile::exists(filepath)) {
QFile old(filepath);
if (!old.remove()) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to remove existing file ")
+ filepath + ": " + old.error());
QFile::remove(tmpname);
reject();
}
}
#endif
// Use ::rename() instead of QFile::rename() to get forced overwrite.
if (rename(QFile::encodeName(tmpname), QFile::encodeName(filepath)) < 0) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to rename ") + tmpname + tr(" to ")
+ filepath + ": " + strerror(errno));
QFile::remove(tmpname);
reject();
return;
}
mainWindow->addRide(filename);
accept();
}

66
src/ManualRideDialog.h Normal file
View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
*
* 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_ManualRideDialog_h
#define _GC_ManualRideDialog_h 1
#include <QtGui>
#include <qdatetimeedit.h>
class MainWindow;
class ManualRideDialog : public QDialog
{
Q_OBJECT
public:
ManualRideDialog(MainWindow *mainWindow, const QDir &home,
bool useMetric);
private slots:
void enterClicked();
void cancelClicked();
void bsEstChanged();
void setBsEst();
private:
void estBSFromDistance();
void estBSFromTime();
bool useMetricUnits;
float timeBS, distanceBS;
MainWindow *mainWindow;
QDir home;
QPushButton *enterButton, *cancelButton;
QLabel *label;
QLabel *hrslbl, *minslbl, *secslbl;
QLineEdit *hrsentry, *minsentry, *secsentry;
QLabel * distancelbl;
QLineEdit *distanceentry;
QLineEdit *HRentry, *BSentry;
QDateTimeEdit *dateTimeEdit;
QRadioButton *estBSbyTimeButton, *estBSbyDistButton;
QVector<unsigned char> records;
QString filename, filepath;
};
#endif // _GC_ManualRideDialog_h

139
src/ManualRideFile.cpp Normal file
View File

@@ -0,0 +1,139 @@
/*
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
*
* 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 "ManualRideFile.h"
#include <QRegExp>
#include <QTextStream>
#include <algorithm> // for std::sort
#include <assert.h>
#include "math.h"
#define MILES_TO_KM 1.609344
#define FEET_TO_METERS 0.3048
static int manualFileReaderRegistered =
RideFileFactory::instance().registerReader("man", new ManualFileReader());
RideFile *ManualFileReader::openRideFile(QFile &file, QStringList &errors) const
{
QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive);
QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive);
bool metric;
int unitsHeader = 2;
/*
* File format:
* "manual"
* "minutes,mph,watts,miles,hr,bikescore" # header (metric or imperial)
* minutes,mph,watts,miles,hr,bikeScore # data
*/
QRegExp manualCSV("manual", Qt::CaseInsensitive);
bool manual = false;
double rideSec;
if (!file.open(QFile::ReadOnly)) {
errors << ("Could not open ride file: \""
+ file.fileName() + "\"");
return NULL;
}
int lineno = 1;
QTextStream is(&file);
RideFile *rideFile = new RideFile();
while (!is.atEnd()) {
// the readLine() method doesn't handle old Macintosh CR line endings
// this workaround will load the the entire file if it has CR endings
// then split and loop through each line
// otherwise, there will be nothing to split and it will read each line as expected.
QString linesIn = is.readLine();
QStringList lines = linesIn.split('\r');
// workaround for empty lines
if(lines.isEmpty()) {
lineno++;
continue;
}
for (int li = 0; li < lines.size(); ++li) {
QString line = lines[li];
if (lineno == 1) {
if (manualCSV.indexIn(line) != -1) {
manual = true;
rideFile->setDeviceType("Manual CSV");
++lineno;
continue;
}
}
else if (lineno == unitsHeader) {
if (metricUnits.indexIn(line) != -1)
metric = true;
else if (englishUnits.indexIn(line) != -1)
metric = false;
else {
errors << ("Can't find units in first line: \"" + line + "\"");
delete rideFile;
file.close();
return NULL;
}
++lineno;
continue;
}
// minutes,kph,watts,km,hr,bikeScore
else if (lineno > unitsHeader) {
double minutes,kph,watts,km,hr,alt,bs;
double cad, nm;
int interval;
minutes = line.section(',', 0, 0).toDouble();
kph = line.section(',', 1, 1).toDouble();
watts = line.section(',', 2, 2).toDouble();
km = line.section(',', 3, 3).toDouble();
hr = line.section(',', 4, 4).toDouble();
bs = line.section(',', 5, 5).toDouble();
if (!metric) {
km *= MILES_TO_KM;
kph *= MILES_TO_KM;
}
cad = nm = 0.0;
interval = 0;
rideFile->appendPoint(minutes * 60.0, cad, hr, km,
kph, nm, watts, alt, interval, bs);
rideSec = minutes * 60.0;
}
++lineno;
}
}
// fix recording interval at ride length:
rideFile->setRecIntSecs(rideSec);
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$");
if (rideTime.indexIn(file.fileName()) >= 0) {
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
rideTime.cap(2).toInt(),
rideTime.cap(3).toInt()),
QTime(rideTime.cap(4).toInt(),
rideTime.cap(5).toInt(),
rideTime.cap(6).toInt()));
rideFile->setStartTime(datetime);
}
file.close();
return rideFile;
}

29
src/ManualRideFile.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
*
* 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 _ManualRideFile_h
#define _ManualRideFile_h
#include "RideFile.h"
struct ManualFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _ManualRideFile_h

172
src/MetricAggregator.cpp Normal file
View File

@@ -0,0 +1,172 @@
/*
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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 "MetricAggregator.h"
#include "DBAccess.h"
#include "RideFile.h"
#include "Zones.h"
#include "Settings.h"
#include "RideItem.h"
#include "RideMetric.h"
#include "TimeUtils.h"
#include <assert.h>
#include <math.h>
#include <QtXml/QtXml>
static char rideFileRegExp[] =
"^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)"
"_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.(raw|srm|csv|tcx)$";
MetricAggregator::MetricAggregator()
{
}
void MetricAggregator::aggregateRides(QDir home, Zones *zones)
{
qDebug() << QDateTime::currentDateTime();
DBAccess *dbaccess = new DBAccess(home);
dbaccess->dropMetricTable();
dbaccess->createDatabase();
QRegExp rx(rideFileRegExp);
QStringList errors;
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
while (i.hasNext()) {
QString name = i.next();
QFile file(home.absolutePath() + "/" + name);
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
importRide(home, zones, ride, name, dbaccess);
}
dbaccess->closeConnection();
delete dbaccess;
qDebug() << QDateTime::currentDateTime();
}
bool MetricAggregator::importRide(QDir path, Zones *zones, RideFile *ride, QString fileName, DBAccess *dbaccess)
{
SummaryMetrics *summaryMetric = new SummaryMetrics();
QFile file(path.absolutePath() + "/" + fileName);
int zone_range = -1;
QRegExp rx(rideFileRegExp);
if (!rx.exactMatch(fileName)) {
fprintf(stderr, "bad name: %s\n", fileName.toAscii().constData());
assert(false);
return false;
}
summaryMetric->setFileName(fileName);
assert(rx.numCaptures() == 7);
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 dateTime(date, time);
summaryMetric->setRideDate(dateTime);
if (zones)
zone_range = zones->whichRange(dateTime.date());
const RideMetricFactory &factory = RideMetricFactory::instance();
QSet<QString> todo;
for (int i = 0; i < factory.metricCount(); ++i)
todo.insert(factory.metricName(i));
while (!todo.empty()) {
QMutableSetIterator<QString> i(todo);
later:
while (i.hasNext()) {
const QString &name = i.next();
const QVector<QString> &deps = factory.dependencies(name);
for (int j = 0; j < deps.size(); ++j)
if (!metrics.contains(deps[j]))
goto later;
RideMetric *metric = factory.newMetric(name);
metric->compute(ride, zones, zone_range, metrics);
metrics.insert(name, metric);
i.remove();
double value = metric->value(true);
if(name == "workout_time")
summaryMetric->setWorkoutTime(value);
else if(name == "average_cad")
summaryMetric->setCadence(value);
else if(name == "total_distance")
summaryMetric->setDistance(value);
else if(name == "skiba_xpower")
summaryMetric->setXPower(value);
else if(name == "average_speed")
summaryMetric->setSpeed(value);
else if(name == "total_work")
summaryMetric->setTotalWork(value);
else if(name == "average_power")
summaryMetric->setWatts(value);
else if(name == "time_riding")
summaryMetric->setRideTime(value);
else if(name == "average_hr")
summaryMetric->setHeartRate(value);
else if(name == "skiba_relative_intensity")
summaryMetric->setRelativeIntensity(value);
else if(name == "skiba_bike_score")
summaryMetric->setBikeScore(value);
}
}
dbaccess->importRide(summaryMetric);
delete summaryMetric;
return true;
}
void MetricAggregator::scanForMissing(QDir home, Zones *zones)
{
QStringList errors;
DBAccess *dbaccess = new DBAccess(home);
QStringList filenames = dbaccess->getAllFileNames();
QRegExp rx(rideFileRegExp);
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
while (i.hasNext()) {
QString name = i.next();
if(!filenames.contains(name))
{
qDebug() << "Found missing file: " << name;
QFile file(home.absolutePath() + "/" + name);
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
importRide(home, zones, ride, name, dbaccess);
}
}
dbaccess->closeConnection();
delete dbaccess;
}
void MetricAggregator::resetMetricTable(QDir home)
{
DBAccess dbAccess(home);
dbAccess.dropMetricTable();
}

49
src/MetricAggregator.h Normal file
View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
*
* 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 METRICAGGREGATOR_H_
#define METRICAGGREGATOR_H_
#include <QMap>
#include "RideFile.h"
#include <QDir>
#include "Zones.h"
#include "RideMetric.h"
#include "DBAccess.h"
class MetricAggregator
{
public:
MetricAggregator();
void aggregateRides(QDir home, Zones *zones);
typedef QHash<QString,RideMetric*> MetricMap;
bool importRide(QDir path, Zones *zones, RideFile *ride, QString fileName, DBAccess *dbaccess);
MetricMap metrics;
void scanForMissing(QDir home, Zones *zones);
void resetMetricTable(QDir home);
};
#endif /* METRICAGGREGATOR_H_ */

344
src/Pages.cpp Normal file
View File

@@ -0,0 +1,344 @@
#include <QtGui>
#include <QIntValidator>
#include <assert.h>
#include "Pages.h"
#include "Settings.h"
ConfigurationPage::~ConfigurationPage()
{
delete configGroup;
delete unitLabel;
delete unitCombo;
delete allRidesAscending;
delete warningLabel;
delete unitLayout;
delete warningLayout;
delete configLayout;
delete mainLayout;
}
ConfigurationPage::ConfigurationPage()
{
configGroup = new QGroupBox(tr("Golden Cheetah Configuration"));
unitLabel = new QLabel(tr("Unit of Measurement:"));
unitCombo = new QComboBox();
unitCombo->addItem(tr("Metric"));
unitCombo->addItem(tr("English"));
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant unit = settings->value(GC_UNIT);
if(unit.toString() == "Metric")
unitCombo->setCurrentIndex(0);
else
unitCombo->setCurrentIndex(1);
QLabel *crankLengthLabel = new QLabel(tr("Crank Length:"));
QVariant crankLength = settings->value(GC_CRANKLENGTH);
crankLengthCombo = new QComboBox();
crankLengthCombo->addItem("160");
crankLengthCombo->addItem("162.5");
crankLengthCombo->addItem("165");
crankLengthCombo->addItem("167.5");
crankLengthCombo->addItem("170");
crankLengthCombo->addItem("172.5");
crankLengthCombo->addItem("175");
crankLengthCombo->addItem("177.5");
crankLengthCombo->addItem("180");
crankLengthCombo->addItem("182.5");
crankLengthCombo->addItem("185");
if(crankLength.toString() == "160")
crankLengthCombo->setCurrentIndex(0);
if(crankLength.toString() == "162.5")
crankLengthCombo->setCurrentIndex(1);
if(crankLength.toString() == "165")
crankLengthCombo->setCurrentIndex(2);
if(crankLength.toString() == "167.5")
crankLengthCombo->setCurrentIndex(3);
if(crankLength.toString() == "170")
crankLengthCombo->setCurrentIndex(4);
if(crankLength.toString() == "172.5")
crankLengthCombo->setCurrentIndex(5);
if(crankLength.toString() == "175")
crankLengthCombo->setCurrentIndex(6);
if(crankLength.toString() == "177.5")
crankLengthCombo->setCurrentIndex(7);
if(crankLength.toString() == "180")
crankLengthCombo->setCurrentIndex(8);
if(crankLength.toString() == "182.5")
crankLengthCombo->setCurrentIndex(9);
if(crankLength.toString() == "185")
crankLengthCombo->setCurrentIndex(10);
allRidesAscending = new QCheckBox("Sort ride list ascending.", this);
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked); // default is ascending sort
if(isAscending.toInt() > 0 ){
allRidesAscending->setCheckState(Qt::Checked);
} else {
allRidesAscending->setCheckState(Qt::Unchecked);
}
warningLabel = new QLabel(tr("Requires Restart To Take Effect"));
unitLayout = new QHBoxLayout;
unitLayout->addWidget(unitLabel);
unitLayout->addWidget(unitCombo);
warningLayout = new QHBoxLayout;
warningLayout->addWidget(warningLabel);
QHBoxLayout *crankLengthLayout = new QHBoxLayout;
crankLengthLayout->addWidget(crankLengthLabel);
crankLengthLayout->addWidget(crankLengthCombo);
// BikeScore Estimate
QVariant BSdays = settings->value(GC_BIKESCOREDAYS);
QVariant BSmode = settings->value(GC_BIKESCOREMODE);
QGridLayout *bsDaysLayout = new QGridLayout;
bsModeLayout = new QHBoxLayout;
QLabel *BSDaysLabel1 = new QLabel(tr("BikeScore Estimate: use rides within last "));
QLabel *BSDaysLabel2 = new QLabel(tr(" days"));
BSdaysEdit = new QLineEdit(BSdays.toString(),this);
BSdaysEdit->setInputMask("009");
QLabel *BSModeLabel = new QLabel(tr("BikeScore estimate mode: "));
bsModeCombo = new QComboBox();
bsModeCombo->addItem("time");
bsModeCombo->addItem("distance");
if (BSmode.toString() == "time")
bsModeCombo->setCurrentIndex(0);
else
bsModeCombo->setCurrentIndex(1);
bsDaysLayout->addWidget(BSDaysLabel1,0,0);
bsDaysLayout->addWidget(BSdaysEdit,0,1);
bsDaysLayout->addWidget(BSDaysLabel2,0,2);
bsModeLayout->addWidget(BSModeLabel);
bsModeLayout->addWidget(bsModeCombo);
configLayout = new QVBoxLayout;
configLayout->addLayout(unitLayout);
configLayout->addWidget(allRidesAscending);
configLayout->addLayout(crankLengthLayout);
configLayout->addLayout(bsDaysLayout);
configLayout->addLayout(bsModeLayout);
configLayout->addLayout(warningLayout);
configGroup->setLayout(configLayout);
mainLayout = new QVBoxLayout;
mainLayout->addWidget(configGroup);
mainLayout->addStretch(1);
setLayout(mainLayout);
}
CyclistPage::~CyclistPage()
{
delete cyclistGroup;
delete lblThreshold;
delete txtThreshold;
delete txtThresholdValidator;
delete btnBack;
delete btnForward;
delete btnDelete;
delete checkboxNew;
delete txtStartDate;
delete txtEndDate;
delete lblStartDate;
delete lblEndDate;
delete calendar;
delete lblCurRange;
delete powerLayout;
delete rangeLayout;
delete dateRangeLayout;
delete zoneLayout;
delete calendarLayout;
delete cyclistLayout;
delete mainLayout;
}
CyclistPage::CyclistPage(Zones **_zones):
zones(_zones)
{
cyclistGroup = new QGroupBox(tr("Cyclist Options"));
lblThreshold = new QLabel(tr("Critical Power:"));
txtThreshold = new QLineEdit();
// the validator will prevent numbers above the upper limit
// from being entered, but will not prevent non-negative numbers
// below the lower limit (since it is still plausible a valid
// entry will result)
txtThresholdValidator = new QIntValidator(20,999,this);
txtThreshold->setValidator(txtThresholdValidator);
btnBack = new QPushButton(this);
btnBack->setText(tr("Back"));
btnForward = new QPushButton(this);
btnForward->setText(tr("Forward"));
btnDelete = new QPushButton(this);
btnDelete->setText(tr("Delete Range"));
checkboxNew = new QCheckBox(this);
checkboxNew->setText(tr("New Range from Date"));
btnForward->setEnabled(false);
txtStartDate = new QLabel("BEGIN");
txtEndDate = new QLabel("END");
lblStartDate = new QLabel("Start: ");
lblStartDate->setAlignment(Qt::AlignRight);
lblEndDate = new QLabel("End: ");
lblEndDate->setAlignment(Qt::AlignRight);
calendar = new QCalendarWidget(this);
lblCurRange = new QLabel(this);
lblCurRange->setFrameStyle(QFrame::Panel | QFrame::Sunken);
lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1));
QDate today = QDate::currentDate();
calendar->setSelectedDate(today);
if ((! *zones) || ((*zones)->getRangeSize() == 0))
setCurrentRange();
else
{
setCurrentRange((*zones)->whichRange(today));
btnDelete->setEnabled(true);
checkboxNew->setCheckState(Qt::Unchecked);
}
int cp = (*zones ? (*zones)->getCP(currentRange) : 0);
if (cp > 0)
setCP(cp);
//Layout
powerLayout = new QHBoxLayout();
powerLayout->addWidget(lblThreshold);
powerLayout->addWidget(txtThreshold);
rangeLayout = new QHBoxLayout();
rangeLayout->addWidget(lblCurRange);
dateRangeLayout = new QHBoxLayout();
dateRangeLayout->addWidget(lblStartDate);
dateRangeLayout->addWidget(txtStartDate);
dateRangeLayout->addWidget(lblEndDate);
dateRangeLayout->addWidget(txtEndDate);
zoneLayout = new QHBoxLayout();
zoneLayout->addWidget(btnBack);
zoneLayout->addWidget(btnForward);
zoneLayout->addWidget(btnDelete);
zoneLayout->addWidget(checkboxNew);
calendarLayout = new QHBoxLayout();
calendarLayout->addWidget(calendar);
cyclistLayout = new QVBoxLayout;
cyclistLayout->addLayout(powerLayout);
cyclistLayout->addLayout(rangeLayout);
cyclistLayout->addLayout(zoneLayout);
cyclistLayout->addLayout(dateRangeLayout);
cyclistLayout->addLayout(calendarLayout);
cyclistGroup->setLayout(cyclistLayout);
mainLayout = new QVBoxLayout;
mainLayout->addWidget(cyclistGroup);
mainLayout->addStretch(1);
setLayout(mainLayout);
}
QString CyclistPage::getText()
{
return txtThreshold->text();
}
int CyclistPage::getCP()
{
int cp = txtThreshold->text().toInt();
return (
(
(cp >= txtThresholdValidator->bottom()) &&
(cp <= txtThresholdValidator->top())
) ?
cp :
0
);
}
void CyclistPage::setCP(int cp)
{
txtThreshold->setText(QString("%1").arg(cp));
}
void CyclistPage::setSelectedDate(QDate date)
{
calendar->setSelectedDate(date);
}
void CyclistPage::setCurrentRange(int range)
{
int num_ranges =
*zones ?
(*zones)->getRangeSize() :
0;
if ((num_ranges == 0) || (range < 0)) {
btnBack->setEnabled(false);
btnDelete->setEnabled(false);
btnForward->setEnabled(false);
calendar->setEnabled(false);
checkboxNew->setCheckState(Qt::Checked);
checkboxNew->setEnabled(false);
currentRange = -1;
lblCurRange->setText("no Current Zone Range");
txtEndDate->setText("undefined");
txtStartDate->setText("undefined");
return;
}
assert ((range >= 0) && (range < num_ranges));
currentRange = range;
// update the labels
lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1));
// update the visibility of the range select buttons
btnForward->setEnabled(currentRange < num_ranges - 1);
btnBack->setEnabled(currentRange > 0);
// if we have ranges to set to, then the calendar must be on
calendar->setEnabled(true);
// update the CP display
setCP((*zones)->getCP(currentRange));
// update date limits
txtStartDate->setText((*zones)->getStartDateString(currentRange));
txtEndDate->setText((*zones)->getEndDateString(currentRange));
}
int CyclistPage::getCurrentRange()
{
return currentRange;
}
bool CyclistPage::isNewMode()
{
return (checkboxNew->checkState() == Qt::Checked);
}

94
src/Pages.h Normal file
View File

@@ -0,0 +1,94 @@
#ifndef PAGES_H
#define PAGES_H
#include <QWidget>
#include <QLineEdit>
#include <QComboBox>
#include <QCalendarWidget>
#include <QPushButton>
#include <QCheckBox>
#include <QList>
#include "Zones.h"
#include <QLabel>
#include <QDateEdit>
#include <QCheckBox>
#include <QValidator>
#include <QGridLayout>
class QGroupBox;
class QHBoxLayout;
class QVBoxLayout;
class ConfigurationPage : public QWidget
{
public:
~ConfigurationPage();
ConfigurationPage();
QComboBox *unitCombo;
QComboBox *crankLengthCombo;
QCheckBox *allRidesAscending;
QLineEdit *BSdaysEdit;
QComboBox *bsModeCombo;
private:
QGroupBox *configGroup;
QLabel *unitLabel;
QLabel *warningLabel;
QHBoxLayout *unitLayout;
QHBoxLayout *warningLayout;
QVBoxLayout *configLayout;
QVBoxLayout *mainLayout;
QGridLayout *bsDaysLayout;
QHBoxLayout *bsModeLayout;
};
class CyclistPage : public QWidget
{
public:
~CyclistPage();
CyclistPage(Zones **_zones);
int thresholdPower;
QString getText();
int getCP();
void setCP(int cp);
void setSelectedDate(QDate date);
void setCurrentRange(int range = -1);
QPushButton *btnBack;
QPushButton *btnForward;
QPushButton *btnDelete;
QCheckBox *checkboxNew;
QCalendarWidget *calendar;
QLabel *lblCurRange;
QLabel *txtStartDate;
QLabel *txtEndDate;
QLabel *lblStartDate;
QLabel *lblEndDate;
int getCurrentRange();
bool isNewMode();
inline void setCPFocus() {
txtThreshold->setFocus();
}
inline QDate selectedDate() {
return calendar->selectedDate();
}
private:
QGroupBox *cyclistGroup;
Zones **zones;
int currentRange;
QLabel *lblThreshold;
QLineEdit *txtThreshold;
QIntValidator *txtThresholdValidator;
QHBoxLayout *powerLayout;
QHBoxLayout *rangeLayout;
QHBoxLayout *dateRangeLayout;
QHBoxLayout *zoneLayout;
QHBoxLayout *calendarLayout;
QVBoxLayout *cyclistLayout;
QVBoxLayout *mainLayout;
};
#endif

450
src/PfPvPlot.cpp Normal file
View File

@@ -0,0 +1,450 @@
/*
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net),
* J.T Conklin (jtc@acorntoolworks.com)
*
* 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 "PfPvPlot.h"
#include "RideFile.h"
#include "RideItem.h"
#include "Settings.h"
#include "Zones.h"
#include <math.h>
#include <assert.h>
#include <qwt_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_marker.h>
#include <qwt_symbol.h>
#include <set>
#define PI M_PI
// Zone labels are drawn if power zone bands are enabled, automatically
// at the center of the plot
class PfPvPlotZoneLabel: public QwtPlotItem
{
private:
PfPvPlot *parent;
int zone_number;
double watts;
QwtText text;
public:
PfPvPlotZoneLabel(PfPvPlot *_parent, int _zone_number)
{
parent = _parent;
zone_number = _zone_number;
RideItem *rideItem = parent->rideItem;
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
setZ(1.0 + zone_number / 100.0);
// create new zone labels if we're shading
if (zones && *zones && (zone_range >= 0)) {
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
QList <QString> zone_names = (*zones)->getZoneNames(zone_range);
int num_zones = zone_lows.size();
assert(zone_names.size() == num_zones);
if (zone_number < num_zones) {
watts =
(
(zone_number + 1 < num_zones) ?
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
(
(zone_number > 0) ?
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
2.0 * zone_lows[zone_number]
)
);
text = QwtText(zone_names[zone_number]);
text.setFont(QFont("Helvetica",24, QFont::Bold));
QColor text_color = zoneColor(zone_number, num_zones);
text_color.setAlpha(64);
text.setColor(text_color);
}
}
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
void draw(QPainter *painter,
const QwtScaleMap &xMap, const QwtScaleMap &yMap,
const QRect &rect) const
{
if (parent->shadeZones() &&
(rect.width() > 0) &&
(rect.height() > 0)
) {
// draw the label along a plot diagonal:
// 1. x*y = watts * dx/dv * dy/df
// 2. x/width = y/height
// =>
// 1. x^2 = width/height * watts
// 2. y^2 = height/width * watts
double xscale = fabs(xMap.transform(3) - xMap.transform(0)) / 3;
double yscale = fabs(yMap.transform(600) - yMap.transform(0)) / 600;
if ((xscale > 0) && (yscale > 0)) {
double w = watts * xscale * yscale;
int x = xMap.transform(sqrt(w * rect.width() / rect.height()) / xscale);
int y = yMap.transform(sqrt(w * rect.height() / rect.width()) / yscale);
// the following code based on source for QwtPlotMarker::draw()
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
tr.moveCenter(QPoint(x, y));
text.draw(painter, tr);
}
}
}
};
QwtArray<double> PfPvPlot::contour_xvalues;
PfPvPlot::PfPvPlot()
: rideItem (NULL),
cp_ (0),
cad_ (85),
cl_ (0.175),
shade_zones(true)
{
setCanvasBackground(Qt::white);
setAxisTitle(yLeft, "Average Effective Pedal Force (N)");
setAxisScale(yLeft, 0, 600);
setAxisTitle(xBottom, "Circumferential Pedal Velocity (m/s)");
setAxisScale(xBottom, 0, 3);
mX = new QwtPlotMarker();
mX->setLineStyle(QwtPlotMarker::VLine);
mX->attach(this);
mY = new QwtPlotMarker();
mY->setLineStyle(QwtPlotMarker::HLine);
mY->attach(this);
cpCurve = new QwtPlotCurve();
cpCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
cpCurve->attach(this);
curve = new QwtPlotCurve();
QwtSymbol sym;
sym.setStyle(QwtSymbol::Ellipse);
sym.setSize(6);
sym.setPen(QPen(Qt::red));
sym.setBrush(QBrush(Qt::NoBrush));
curve->setSymbol(sym);
curve->setStyle(QwtPlotCurve::Dots);
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
curve->attach(this);
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
cl_ = settings->value(GC_CRANKLENGTH).toDouble() / 1000.0;
recalc();
}
void
PfPvPlot::refreshZoneItems()
{
// clear out any zone curves which are presently defined
if (zoneCurves.size()) {
QListIterator<QwtPlotCurve *> i(zoneCurves);
while (i.hasNext()) {
QwtPlotCurve *curve = i.next();
curve->detach();
delete curve;
}
}
zoneCurves.clear();
// delete any existing power zone labels
if (zoneLabels.size()) {
QListIterator<PfPvPlotZoneLabel *> i(zoneLabels);
while (i.hasNext()) {
PfPvPlotZoneLabel *label = i.next();
label->detach();
delete label;
}
}
zoneLabels.clear();
if (! rideItem)
return;
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
if (zones && *zones && (zone_range >= 0)) {
setCP((*zones)->getCP(zone_range));
// populate the zone curves
QList <int> zone_power = (*zones)->getZoneLows(zone_range);
QList <QString> zone_name = (*zones)->getZoneNames(zone_range);
int num_zones = zone_power.size();
assert(zone_name.size() == num_zones);
if (num_zones > 0) {
QPen *pen = new QPen();
pen->setStyle(Qt::NoPen);
QwtArray<double> yvalues;
// generate x values
for (int z = 0; z < num_zones; z ++) {
QwtPlotCurve *curve;
curve = new QwtPlotCurve(zone_name[z]);
curve->setPen(*pen);
QColor brush_color = zoneColor(z, num_zones);
brush_color.setHsv(
brush_color.hue(),
brush_color.saturation() / 4,
brush_color.value()
);
curve->setBrush(brush_color); // fill below the line
curve->setZ(1 - 1e-6 * zone_power[z]);
// generate data for curve
if (z < num_zones - 1) {
QwtArray <double> contour_yvalues;
int watts = zone_power[z + 1];
int dwatts = (double) watts;
for (int i = 0; i < contour_xvalues.size(); i ++)
contour_yvalues.append(
(1e6 * contour_xvalues[i] < watts) ?
1e6 :
dwatts / contour_xvalues[i]
);
curve->setData(contour_xvalues, contour_yvalues);
}
else {
// top zone has a curve at "infinite" power
QwtArray <double> contour_x;
QwtArray <double> contour_y;
contour_x.append(contour_xvalues[0]);
contour_x.append(contour_xvalues[contour_xvalues.size() - 1]);
contour_y.append(1e6);
contour_y.append(1e6);
curve->setData(contour_x, contour_y);
}
curve->setVisible(shade_zones);
curve->attach(this);
zoneCurves.append(curve);
}
delete pen;
// generate labels for existing zones
for (int z = 0; z < num_zones; z ++) {
PfPvPlotZoneLabel *label = new PfPvPlotZoneLabel(this, z);
label->setVisible(shade_zones);
label->attach(this);
zoneLabels.append(label);
}
// get the zones visible, even if data may take awhile
replot();
}
}
}
void
PfPvPlot::setData(RideItem *_rideItem)
{
rideItem = _rideItem;
RideFile *ride = rideItem->ride;
if (ride) {
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
// quickly erase old data
curve->setVisible(false);
// handle zone stuff
refreshZoneItems();
// due to the discrete power and cadence values returned by the
// power meter, there will very likely be many duplicate values.
// Rather than pass them all to the curve, use a set to strip
// out duplicates.
std::set<std::pair<double, double> > dataSet;
long tot_cad = 0;
long tot_cad_points = 0;
QListIterator<RideFilePoint*> i(ride->dataPoints());
while (i.hasNext()) {
const RideFilePoint *p1 = i.next();
if (p1->watts != 0 && p1->cad != 0) {
double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;
dataSet.insert(std::make_pair<double, double>(aepf, cpv));
tot_cad += p1->cad;
tot_cad_points++;
}
}
if (tot_cad_points == 0) {
setTitle("no cadence");
refreshZoneItems();
curve->setVisible(false);
}
else {
// Now that we have the set of points, transform them into the
// QwtArrays needed to set the curve's data.
QwtArray<double> aepfArray;
QwtArray<double> cpvArray;
std::set<std::pair<double, double> >::const_iterator j(dataSet.begin());
while (j != dataSet.end()) {
const std::pair<double, double>& dataPoint = *j;
aepfArray.push_back(dataPoint.first);
cpvArray.push_back(dataPoint.second);
++j;
}
setCAD(tot_cad / tot_cad_points);
curve->setData(cpvArray, aepfArray);
// now show the data (zone shading would already be visible)
curve->setVisible(true);
}
}
else {
setTitle("no data");
refreshZoneItems();
curve->setVisible(false);
}
replot();
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
setCL(settings->value(GC_CRANKLENGTH).toDouble() / 1000.0);
}
void
PfPvPlot::recalc()
{
// initialize x values used for contours
if (contour_xvalues.isEmpty()) {
for (double x = 0; x <= 3.0; x += x / 20 + 0.02)
contour_xvalues.append(x);
contour_xvalues.append(3.0);
}
double cpv = (cad_ * cl_ * 2.0 * PI) / 60.0;
mX->setXValue(cpv);
double aepf = (cp_ * 60.0) / (cad_ * cl_ * 2.0 * PI);
mY->setYValue(aepf);
QwtArray<double> yvalues(contour_xvalues.size());
if (cp_) {
for (int i = 0; i < contour_xvalues.size(); i ++)
yvalues[i] =
(cpv < cp_ / 1e6) ?
1e6 :
cp_ / contour_xvalues[i];
// generate curve at a given power
cpCurve->setData(contour_xvalues, yvalues);
}
else
// an empty curve if no power (or zero power) is specified
cpCurve->setData(QwtArray <double>::QwtArray(), QwtArray <double>::QwtArray());
replot();
}
int
PfPvPlot::getCP()
{
return cp_;
}
void
PfPvPlot::setCP(int cp)
{
cp_ = cp;
recalc();
emit changedCP( QString("%1").arg(cp) );
}
int
PfPvPlot::getCAD()
{
return cad_;
}
void
PfPvPlot::setCAD(int cadence)
{
cad_ = cadence;
recalc();
emit changedCAD( QString("%1").arg(cadence) );
}
double
PfPvPlot::getCL()
{
return cl_;
}
void
PfPvPlot::setCL(double cranklen)
{
cl_ = cranklen;
recalc();
emit changedCL( QString("%1").arg(cranklen) );
}
// process checkbox for zone shading
void
PfPvPlot::setShadeZones(bool value)
{
shade_zones = value;
// if there are defined zones and labels, set their visibility
for (int i = 0; i < zoneCurves.size(); i ++)
zoneCurves[i]->setVisible(shade_zones);
for (int i = 0; i < zoneLabels.size(); i ++)
zoneLabels[i]->setVisible(shade_zones);
replot();
}

79
src/PfPvPlot.h Normal file
View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net),
* J.T Conklin (jtc@acorntoolworks.com)
*
* 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_QaPlot_h
#define _GC_QaPlot_h 1
#include <qwt_plot.h>
// forward references
class RideFile;
class RideItem;
class QwtPlotCurve;
class QwtPlotMarker;
class PfPvPlotZoneLabel;
class PfPvPlot : public QwtPlot
{
Q_OBJECT
public:
PfPvPlot();
void refreshZoneItems();
void setData(RideItem *_rideItem);
int getCP();
void setCP(int cp);
int getCAD();
void setCAD(int cadence);
double getCL();
void setCL(double cranklen);
void recalc();
RideItem *rideItem;
bool shadeZones() const { return shade_zones; }
void setShadeZones(bool value);
public slots:
signals:
void changedCP( const QString& );
void changedCAD( const QString& );
void changedCL( const QString& );
protected:
QwtPlotCurve *curve;
QwtPlotCurve *cpCurve;
QList <QwtPlotCurve *> zoneCurves;
QList <PfPvPlotZoneLabel *> zoneLabels;
QwtPlotMarker *mX;
QwtPlotMarker *mY;
static QwtArray<double> contour_xvalues; // values used in CP and contour plots: djconnel
int cp_;
int cad_;
double cl_;
bool shade_zones; // whether to shade zones, added 27Apr2009 djconnel
};
#endif // _GC_QaPlot_h

227
src/PolarRideFile.cpp Normal file
View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2007 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 "PolarRideFile.h"
#include <QRegExp>
#include <QTextStream>
#include <algorithm> // for std::sort
#include <assert.h>
#include "math.h"
static int polarFileReaderRegistered =
RideFileFactory::instance().registerReader("hrm", new PolarFileReader());
RideFile *PolarFileReader::openRideFile(QFile &file, QStringList &errors) const
{
QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive);
QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive);
// bool metric;
QDate date;
QString note("");
double version=0;
double seconds=0;
double distance=0;
int interval = 0;
bool speed = false;
bool cadence = false;
bool altitude = false;
bool power = false;
bool balance = false;
bool pedaling_index = false;
int recInterval = 1;
if (!file.open(QFile::ReadOnly)) {
errors << ("Could not open ride file: \""
+ file.fileName() + "\"");
return NULL;
}
int lineno = 1;
double next_interval=0;
QList<double> intervals;
QTextStream is(&file);
RideFile *rideFile = new RideFile();
QString section = NULL;
while (!is.atEnd()) {
// the readLine() method doesn't handle old Macintosh CR line endings
// this workaround will load the the entire file if it has CR endings
// then split and loop through each line
// otherwise, there will be nothing to split and it will read each line as expected.
QString linesIn = is.readLine();
QStringList lines = linesIn.split('\r');
// workaround for empty lines
if(lines.size() == 0) {
lineno++;
continue;
}
for (int li = 0; li < lines.size(); ++li) {
QString line = lines[li];
if (line == "") {
}
else if (line.startsWith("[")) {
//fprintf(stderr, "section : %s\n", line.toAscii().constData());
section=line;
if (section == "[HRData]")
next_interval = intervals.at(0);
}
else if (section == "[Params]"){
if (line.contains("Version=")) {
QString versionString = QString(line);
versionString.remove(0,8).insert(1, ".");
version = versionString.toFloat();
rideFile->setDeviceType("Polar HRM (v"+versionString+")");
} else if (line.contains("SMode=")) {
line.remove(0,6);
QString smode = QString(line);
if (smode.at(0)=='1')
speed = true;
if (smode.length()>0 && smode.at(1)=='1')
cadence = true;
if (smode.length()>1 && smode.at(2)=='1')
altitude = true;
if (smode.length()>2 && smode.at(3)=='1')
power = true;
if (smode.length()>3 && smode.at(4)=='1')
balance = true;
if (smode.length()>4 && smode.at(5)=='1')
pedaling_index = true;
/*
It appears that the Polar CS600 exports its data alays in metric when downloaded from the
polar software even when English units are displayed on the unit.. It also never sets
this bit low in the .hrm file. This will have to get changed if other software downloads
this differently
*/
// if (smode.length()>6 && smode.at(7)=='1')
// metric = true;
} else if (line.contains("Interval=")) {
recInterval = line.remove(0,9).toInt();
rideFile->setRecIntSecs(recInterval);
} else if (line.contains("Date=")) {
line.remove(0,5);
date= QDate(line.left(4).toInt(),
line.mid(4,2).toInt(),
line.mid(6,2).toInt());
} else if (line.contains("StartTime=")) {
line.remove(0,10);
QDateTime datetime(date,
QTime(line.left(2).toInt(),
line.mid(3,2).toInt(),
line.mid(6,2).toInt()));
rideFile->setStartTime(datetime);
}
}
else if (section == "[Note]"){
note.append(line);
}
else if (section == "[IntTimes]"){
double int_seconds = line.left(2).toInt()*60*60+line.mid(3,2).toInt()*60+line.mid(6,3).toFloat();
intervals.append(int_seconds);
if (lines.size()==1) {
is.readLine();
is.readLine();
if (version>1.05) {
is.readLine();
is.readLine();
}
} else {
li+=2;
if (version>1.05)
li+=2;
}
}
else if (section == "[HRData]"){
double nm=0,kph=0,watts=0,km=0,cad=0,hr=0,alt=0;
seconds += recInterval;
int i=0;
hr = line.section('\t', i, i).toDouble();
i++;
if (speed) {
kph = line.section('\t', i, i).toDouble()/10;
i++;
}
if (cadence) {
cad = line.section('\t', i, i).toDouble();
i++;
}
if (altitude) {
alt = line.section('\t', i, i).toDouble();
i++;
}
if (power) {
watts = line.section('\t', i, i).toDouble();
}
distance = distance + kph/60/60*recInterval;
km = distance;
if (next_interval < seconds) {
interval = intervals.indexOf(next_interval);
if (intervals.count()>interval+1){
interval++;
next_interval = intervals.at(interval);
}
}
rideFile->appendPoint(seconds, cad, hr, km, kph, nm, watts, alt, interval);
//fprintf(stderr, " %f, %f, %f, %f, %f, %f, %f, %d\n", seconds, cad, hr, km, kph, nm, watts, alt, interval);
}
++lineno;
}
}
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.hrm$");
if (rideTime.indexIn(file.fileName()) >= 0) {
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
rideTime.cap(2).toInt(),
rideTime.cap(3).toInt()),
QTime(rideTime.cap(4).toInt(),
rideTime.cap(5).toInt(),
rideTime.cap(6).toInt()));
rideFile->setStartTime(datetime);
}
file.close();
return rideFile;
}

29
src/PolarRideFile.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2007 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 _PolarRideFile_h
#define _PolarRideFile_h
#include "RideFile.h"
struct PolarFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _PolarRideFile_h

643
src/PowerHist.cpp Normal file
View File

@@ -0,0 +1,643 @@
/*
* 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 "PowerHist.h"
#include "RideItem.h"
#include "RideFile.h"
#include "Settings.h"
#include "Zones.h"
#include <assert.h>
#include <qpainter.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_zoomer.h>
#include <qwt_scale_engine.h>
#include <qwt_text.h>
#include <qwt_legend.h>
#include <qwt_data.h>
class penTooltip: public QwtPlotZoomer
{
public:
penTooltip(QwtPlotCanvas *canvas):
QwtPlotZoomer(canvas)
{
// With some versions of Qt/Qwt, setting this to AlwaysOn
// causes an infinite recursion.
//setTrackerMode(AlwaysOn);
setTrackerMode(AlwaysOff);
}
virtual QwtText trackerText(const QwtDoublePoint &pos) const
{
QColor bg(Qt::white);
#if QT_VERSION >= 0x040300
bg.setAlpha(200);
#endif
QwtText text = QString("%1").arg((int)pos.x());
text.setBackgroundBrush( QBrush( bg ));
return text;
}
};
// define a background class to handle shading of power zones
// draws power zone bands IF zones are defined and the option
// to draw bonds has been selected
class PowerHistBackground: public QwtPlotItem
{
private:
PowerHist *parent;
public:
PowerHistBackground(PowerHist *_parent)
{
setZ(0.0);
parent = _parent;
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
virtual void draw(QPainter *painter,
const QwtScaleMap &xMap, const QwtScaleMap &,
const QRect &rect) const
{
RideItem *rideItem = parent->rideItem;
if (! rideItem)
return;
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
int num_zones = zone_lows.size();
if (num_zones > 0) {
for (int z = 0; z < num_zones; z ++) {
QRect r = rect;
QColor shading_color =
zoneColor(z, num_zones);
shading_color.setHsv(
shading_color.hue(),
shading_color.saturation() / 4,
shading_color.value()
);
r.setLeft(xMap.transform(zone_lows[z]));
if (z + 1 < num_zones)
r.setRight(xMap.transform(zone_lows[z + 1]));
if (r.right() >= r.left())
painter->fillRect(r, shading_color);
}
}
}
}
};
// Zone labels are drawn if power zone bands are enabled, automatically
// at the center of the plot
class PowerHistZoneLabel: public QwtPlotItem
{
private:
PowerHist *parent;
int zone_number;
double watts;
QwtText text;
public:
PowerHistZoneLabel(PowerHist *_parent, int _zone_number)
{
parent = _parent;
zone_number = _zone_number;
RideItem *rideItem = parent->rideItem;
if (! rideItem)
return;
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
setZ(1.0 + zone_number / 100.0);
// create new zone labels if we're shading
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
QList <QString> zone_names = (*zones)->getZoneNames(zone_range);
int num_zones = zone_lows.size();
assert(zone_names.size() == num_zones);
if (zone_number < num_zones) {
watts =
(
(zone_number + 1 < num_zones) ?
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
(
(zone_number > 0) ?
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
2.0 * zone_lows[zone_number]
)
);
text = QwtText(zone_names[zone_number]);
text.setFont(QFont("Helvetica",24, QFont::Bold));
QColor text_color = zoneColor(zone_number, num_zones);
text_color.setAlpha(64);
text.setColor(text_color);
}
}
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
void draw(QPainter *painter,
const QwtScaleMap &xMap, const QwtScaleMap &,
const QRect &rect) const
{
if (parent->shadeZones()) {
int x = xMap.transform(watts);
int y = (rect.bottom() + rect.top()) / 2;
// the following code based on source for QwtPlotMarker::draw()
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
tr.moveCenter(QPoint(y, -x));
painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed
text.draw(painter, tr);
}
}
};
PowerHist::PowerHist():
selected(wattsShaded),
rideItem(NULL),
binw(20),
withz(true),
settings(NULL),
unit(0),
lny(false)
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
unit = settings->value(GC_UNIT);
useMetricUnits = (unit.toString() == "Metric");
// create a background object for shading
bg = new PowerHistBackground(this);
bg->attach(this);
setCanvasBackground(Qt::white);
setParameterAxisTitle();
setAxisTitle(yLeft, "Cumulative Time (minutes)");
curve = new QwtPlotCurve("");
curve->setStyle(QwtPlotCurve::Steps);
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen *pen = new QPen(Qt::black);
pen->setWidth(2.0);
curve->setPen(*pen);
QColor brush_color = Qt::black;
brush_color.setAlpha(64);
curve->setBrush(brush_color); // fill below the line
delete pen;
curve->attach(this);
grid = new QwtPlotGrid();
grid->enableX(false);
QPen gridPen;
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
grid->attach(this);
zoneLabels = QList <PowerHistZoneLabel *>::QList();
new penTooltip(this->canvas());
}
PowerHist::~PowerHist() {
delete bg;
delete curve;
delete grid;
}
// static const variables from PoweHist.h:
// discritized unit for smoothing
const double PowerHist::wattsDelta;
const double PowerHist::nmDelta;
const double PowerHist::hrDelta;
const double PowerHist::kphDelta;
const double PowerHist::cadDelta;
// digits for text entry validator
const int PowerHist::wattsDigits;
const int PowerHist::nmDigits;
const int PowerHist::hrDigits;
const int PowerHist::kphDigits;
const int PowerHist::cadDigits;
void
PowerHist::refreshZoneLabels()
{
// delete any existing power zone labels
if (zoneLabels.size()) {
QListIterator<PowerHistZoneLabel *> i(zoneLabels);
while (i.hasNext()) {
PowerHistZoneLabel *label = i.next();
label->detach();
delete label;
}
}
zoneLabels.clear();
if (! rideItem)
return;
if ((selected == wattsShaded) || (selected == wattsUnshaded)) {
Zones **zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
// generate labels for existing zones
if (zones && *zones && (zone_range >= 0)) {
int num_zones = (*zones)->numZones(zone_range);
for (int z = 0; z < num_zones; z ++) {
PowerHistZoneLabel *label = new PowerHistZoneLabel(this, z);
label->attach(this);
zoneLabels.append(label);
}
}
}
}
void
PowerHist::recalc()
{
QVector<unsigned int> *array;
int arrayLength = 0;
double delta;
// make sure the interval length is set
if (dt <= 0)
return;
if ((selected == wattsShaded) ||
(selected == wattsUnshaded)
) {
array = &wattsArray;
delta = wattsDelta;
arrayLength = wattsArray.size();
}
else if (selected == nm) {
array = &nmArray;
delta = nmDelta;
arrayLength = nmArray.size();
}
else if (selected == hr) {
array = &hrArray;
delta = hrDelta;
arrayLength = hrArray.size();
}
else if (selected == kph) {
array = &kphArray;
delta = kphDelta;
arrayLength = kphArray.size();
}
else if (selected == cad) {
array = &cadArray;
delta = cadDelta;
arrayLength = cadArray.size();
}
if (!array)
return;
int count = int(ceil((arrayLength - 1) / binw));
// allocate space for data, plus beginning and ending point
QVector<double> parameterValue(count+2);
QVector<double> totalTime(count+2);
int i;
for (i = 1; i <= count; ++i) {
int high = i * binw;
int low = high - binw;
if (low==0 && !withz)
low++;
parameterValue[i] = high * delta;
totalTime[i] = 1e-9; // nonzero to accomodate log plot
while (low < high)
totalTime[i] += dt * (*array)[low++];
}
totalTime[i] = 1e-9; // nonzero to accomodate log plot
parameterValue[i] = i * delta * binw;
totalTime[0] = 1e-9;
parameterValue[0] = 0;
curve->setData(parameterValue.data(), totalTime.data(), count + 2);
setAxisScale(xBottom, 0.0, parameterValue[count + 1]);
refreshZoneLabels();
setYMax();
replot();
}
void
PowerHist::setYMax()
{
static const double tmin = 1.0/60;
setAxisScale(yLeft, (lny ? tmin : 0.0), curve->maxYValue() * 1.1);
}
void
PowerHist::setData(RideItem *_rideItem)
{
rideItem = _rideItem;
RideFile *ride = rideItem->ride;
if (ride) {
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
static const int maxSize = 4096;
// recording interval in minutes
dt = ride->recIntSecs() / 60.0;
wattsArray.resize(0);
nmArray.resize(0);
hrArray.resize(0);
kphArray.resize(0);
cadArray.resize(0);
QListIterator<RideFilePoint*> j(ride->dataPoints());
// unit conversion factor for imperial units for selected parameters
double torque_factor = (useMetricUnits ? 1.0 : 0.73756215);
double speed_factor = (useMetricUnits ? 1.0 : 0.62137119);
while (j.hasNext()) {
const RideFilePoint *p1 = j.next();
int wattsIndex = int(floor(p1->watts / wattsDelta));
if (wattsIndex >= 0 && wattsIndex < maxSize) {
if (wattsIndex >= wattsArray.size())
wattsArray.resize(wattsIndex + 1);
wattsArray[wattsIndex]++;
}
int nmIndex = int(floor(p1->nm * torque_factor / nmDelta));
if (nmIndex >= 0 && nmIndex < maxSize) {
if (nmIndex >= nmArray.size())
nmArray.resize(nmIndex + 1);
nmArray[nmIndex]++;
}
int hrIndex = int(floor(p1->hr / hrDelta));
if (hrIndex >= 0 && hrIndex < maxSize) {
if (hrIndex >= hrArray.size())
hrArray.resize(hrIndex + 1);
hrArray[hrIndex]++;
}
int kphIndex = int(floor(p1->kph * speed_factor / kphDelta));
if (kphIndex >= 0 && kphIndex < maxSize) {
if (kphIndex >= kphArray.size())
kphArray.resize(kphIndex + 1);
kphArray[kphIndex]++;
}
int cadIndex = int(floor(p1->cad / cadDelta));
if (cadIndex >= 0 && cadIndex < maxSize) {
if (cadIndex >= cadArray.size())
cadArray.resize(cadIndex + 1);
cadArray[cadIndex]++;
}
}
recalc();
}
else {
setTitle("no data");
replot();
}
}
void
PowerHist::setBinWidth(int value)
{
binw = value;
recalc();
}
double
PowerHist::getDelta()
{
switch (selected) {
case wattsShaded:
case wattsUnshaded:
return wattsDelta;
case nm:
return nmDelta;
case hr:
return hrDelta;
case kph:
return kphDelta;
case cad:
return cadDelta;
}
return 1;
}
int
PowerHist::getDigits()
{
switch (selected) {
case wattsShaded:
case wattsUnshaded:
return wattsDigits;
case nm:
return nmDigits;
case hr:
return hrDigits;
case kph:
return kphDigits;
case cad:
return cadDigits;
}
return 1;
}
int
PowerHist::setBinWidthRealUnits(double value)
{
setBinWidth(round(value / getDelta()));
return binw;
}
double
PowerHist::getBinWidthRealUnits()
{
return binw * getDelta();
}
void
PowerHist::setWithZeros(bool value)
{
withz = value;
recalc();
}
void
PowerHist::setlnY(bool value)
{
// note: setAxisScaleEngine deletes the old ScaleEngine, so specifying
// "new" in the argument list is not a leak
lny=value;
if (lny)
{
setAxisScaleEngine(yLeft, new QwtLog10ScaleEngine);
curve->setBaseline(1e-6);
}
else
{
setAxisScaleEngine(yLeft, new QwtLinearScaleEngine);
curve->setBaseline(0);
}
setYMax();
replot();
}
void
PowerHist::setParameterAxisTitle()
{
setAxisTitle(
xBottom,
((selected == wattsShaded) ||
(selected == wattsUnshaded)
) ?
"watts" :
((selected == hr) ?
"beats/minute" :
((selected == cad) ?
"revolutions/min" :
useMetricUnits ?
((selected == nm) ?
"newton-meters" :
((selected == kph) ?
"km/hr" :
"undefined"
)
) :
((selected == nm) ?
"ft-lb" :
((selected == kph) ?
"miles/hr" :
"undefined"
)
)
)
)
);
}
void
PowerHist::setSelection(Selection selection) {
if (selected == selection)
return;
selected = selection;
setParameterAxisTitle();
recalc();
}
void
PowerHist::fixSelection() {
Selection s = selected;
RideFile *ride = rideItem->ride;
if (ride)
do
{
if ((s == wattsShaded) || (s == wattsUnshaded))
{
if (ride->areDataPresent()->watts)
setSelection(s);
else
s = nm;
}
else if (s == nm)
{
if (ride->areDataPresent()->nm)
setSelection(s);
else
s = hr;
}
else if (s == hr)
{
if (ride->areDataPresent()->hr)
setSelection(s);
else
s = kph;
}
else if (s == kph)
{
if (ride->areDataPresent()->kph)
setSelection(s);
else
s = cad;
}
else if (s == cad)
{
if (ride->areDataPresent()->cad)
setSelection(s);
else
s = wattsShaded;
}
} while (s != selected);
}
bool PowerHist::shadeZones() const
{
return (
rideItem &&
rideItem->ride &&
selected == wattsShaded
);
}

126
src/PowerHist.h Normal file
View File

@@ -0,0 +1,126 @@
/*
* 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_PowerHist_h
#define _GC_PowerHist_h 1
#include <qwt_plot.h>
#include <qsettings.h>
#include <qvariant.h>
class QwtPlotCurve;
class QwtPlotGrid;
class RideItem;
class PowerHistBackground;
class PowerHistZoneLabel;
class PowerHist : public QwtPlot
{
Q_OBJECT
public:
QwtPlotCurve *curve;
QList <PowerHistZoneLabel *> zoneLabels;
PowerHist();
~PowerHist();
int binWidth() const { return binw; }
inline bool islnY() const { return lny; }
inline bool withZeros() const { return withz; }
bool shadeZones() const;
enum Selection {
wattsShaded,
wattsUnshaded,
nm,
hr,
kph,
cad
} selected;
inline Selection selection() { return selected; }
void setData(RideItem *_rideItem);
void setSelection(Selection selection);
void fixSelection();
void setBinWidth(int value);
double getDelta();
int getDigits();
double getBinWidthRealUnits();
int setBinWidthRealUnits(double value);
void refreshZoneLabels();
RideItem *rideItem;
public slots:
void setlnY(bool value);
void setWithZeros(bool value);
protected:
QwtPlotGrid *grid;
// storage for data counts
QVector<unsigned int>
wattsArray,
nmArray,
hrArray,
kphArray,
cadArray;
int binw;
bool withz; // whether zeros are included in histogram
double dt; // length of sample
void recalc();
void setYMax();
private:
QSettings *settings;
QVariant unit;
PowerHistBackground *bg;
bool lny;
// discritized unit for smoothing
static const double wattsDelta = 1.0;
static const double nmDelta = 0.1;
static const double hrDelta = 1.0;
static const double kphDelta = 0.1;
static const double cadDelta = 1.0;
// digits for text entry validator
static const int wattsDigits = 0;
static const int nmDigits = 1;
static const int hrDigits = 0;
static const int kphDigits = 1;
static const int cadDigits = 0;
void setParameterAxisTitle();
bool useMetricUnits; // whether metric units are used (or imperial)
};
#endif // _GC_PowerHist_h

327
src/PowerTapDevice.cpp Normal file
View File

@@ -0,0 +1,327 @@
/*
* Copyright (c) 2008 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 "PowerTapDevice.h"
#include "PowerTapUtil.h"
#include <math.h>
#define PT_DEBUG false
static bool powerTapRegistered =
Device::addDevice("PowerTap", new PowerTapDevice());
QString
PowerTapDevice::downloadInstructions() const
{
return ("Make sure the PowerTap unit is turned\n"
"on and that its display says, \"Host\"");
}
static bool
hasNewline(const char *buf, int len)
{
static char newline[] = { 0x0d, 0x0a };
if (len < 2)
return false;
for (int i = 0; i < len; ++i) {
bool success = true;
for (int j = 0; j < 2; ++j) {
if (buf[i+j] != newline[j]) {
success = false;
break;
}
}
if (success)
return true;
}
return false;
}
static QString
cEscape(const char *buf, int len)
{
char *result = new char[4 * len + 1];
char *tmp = result;
for (int i = 0; i < len; ++i) {
if (buf[i] == '"')
tmp += sprintf(tmp, "\\\"");
else if (isprint(buf[i]))
*(tmp++) = buf[i];
else
tmp += sprintf(tmp, "\\x%02x", 0xff & (unsigned) buf[i]);
}
return result;
}
static bool
doWrite(CommPortPtr dev, char c, bool hwecho, QString &err)
{
if (PT_DEBUG) printf("writing '%c' to device\n", c);
int n = dev->write(&c, 1, err);
if (n != 1) {
if (n < 0)
err = QString("failed to write %1 to device: %2").arg(c).arg(err);
else
err = QString("timeout writing %1 to device").arg(c);
return false;
}
if (hwecho) {
char c;
int n = dev->read(&c, 1, err);
if (n != 1) {
if (n < 0)
err = QString("failed to read back hardware echo: %2").arg(err);
else
err = "timeout reading back hardware echo";
return false;
}
}
return true;
}
static int
readUntilNewline(CommPortPtr dev, char *buf, int len, QString &err)
{
int sofar = 0;
while (!hasNewline(buf, sofar)) {
assert(sofar < len);
// Read one byte at a time to avoid waiting for timeout.
int n = dev->read(buf + sofar, 1, err);
if (n <= 0) {
err = (n < 0) ? ("read error: " + err) : "read timeout";
err += QString(", read %1 bytes so far: \"%2\"")
.arg(sofar).arg(cEscape(buf, sofar));
return -1;
}
sofar += n;
}
return sofar;
}
bool
PowerTapDevice::download(CommPortPtr dev, const QDir &tmpdir,
QString &tmpname, QString &filename,
StatusCallback statusCallback, QString &err)
{
if (!dev->open(err)) {
err = "ERROR: open failed: " + err;
return false;
}
// make several attempts at reading the version
int attempts = 3;
QString cbtext;
int veridx = -1;
int version_len;
char vbuf[256];
QByteArray version;
do {
if (!doWrite(dev, 0x56, false, err)) // 'V'
return false;
cbtext = "Reading version...";
if (!statusCallback(cbtext)) {
err = "download cancelled";
return false;
}
version_len = readUntilNewline(dev, vbuf, sizeof(vbuf), err);
if (version_len < 0) {
err = "Error reading version: " + err;
return false;
}
if (PT_DEBUG) {
printf("read version \"%s\"\n",
cEscape(vbuf, version_len).toAscii().constData());
}
version = QByteArray(vbuf, version_len);
// We expect the version string to be something like
// "VER 02.21 PRO...", so if we see two V's, it's probably
// because there's a hardware echo going on.
veridx = version.indexOf("VER");
} while ((--attempts > 0) && (veridx < 0));
if (veridx < 0) {
err = QString("Unrecognized version \"%1\"")
.arg(cEscape(vbuf, version_len));
return false;
}
bool hwecho = version.indexOf('V') < veridx;
if (PT_DEBUG) printf("hwecho=%s\n", hwecho ? "true" : "false");
cbtext += "done.\nReading header...";
if (!statusCallback(cbtext)) {
err = "download cancelled";
return false;
}
if (!doWrite(dev, 0x44, hwecho, err)) // 'D'
return false;
unsigned char header[6];
int header_len = dev->read(header, sizeof(header), err);
if (header_len != 6) {
if (header_len < 0)
err = "ERROR: reading header: " + err;
else
err = "ERROR: timeout reading header";
return false;
}
if (PT_DEBUG) {
printf("read header \"%s\"\n",
cEscape((char*) header,
sizeof(header)).toAscii().constData());
}
QVector<unsigned char> records;
for (size_t i = 0; i < sizeof(header); ++i)
records.append(header[i]);
cbtext += "done.\nReading ride data...\n";
if (!statusCallback(cbtext)) {
err = "download cancelled";
return false;
}
int cbtextlen = cbtext.length();
double recIntSecs = 0.0;
fflush(stdout);
while (true) {
if (PT_DEBUG) printf("reading block\n");
unsigned char buf[256 * 6 + 1];
int n = dev->read(buf, 2, err);
if (n < 2) {
if (n < 0)
err = "ERROR: reading first two: " + err;
else
err = "ERROR: timeout reading first two";
return false;
}
if (PT_DEBUG) {
printf("read 2 bytes: \"%s\"\n",
cEscape((char*) buf, 2).toAscii().constData());
}
if (hasNewline((char*) buf, 2))
break;
unsigned count = 2;
while (count < sizeof(buf)) {
n = dev->read(buf + count, sizeof(buf) - count, err);
if (n < 0) {
err = "ERROR: reading block: " + err;
return false;
}
if (n == 0) {
err = "ERROR: timeout reading block";
return false;
}
if (PT_DEBUG) {
printf("read %d bytes: \"%s\"\n", n,
cEscape((char*) buf + count, n).toAscii().constData());
}
count += n;
}
unsigned csum = 0;
for (int i = 0; i < ((int) sizeof(buf)) - 1; ++i)
csum += buf[i];
if ((csum % 256) != buf[sizeof(buf) - 1]) {
err = "ERROR: bad checksum";
return false;
}
if (PT_DEBUG) printf("good checksum\n");
for (size_t i = 0; i < sizeof(buf) - 1; ++i)
records.append(buf[i]);
if (recIntSecs == 0.0) {
unsigned char *data = records.data();
bool bIsVer81 = PowerTapUtil::is_Ver81(data);
for (int i = 0; i < records.size(); i += 6) {
if (PowerTapUtil::is_config(data + i, bIsVer81)) {
unsigned unused1, unused2, unused3;
PowerTapUtil::unpack_config(
data + i, &unused1, &unused2,
&recIntSecs, &unused3, bIsVer81);
}
}
}
if (recIntSecs != 0.0) {
int min = (int) round(records.size() / 6 * recIntSecs);
cbtext.chop(cbtext.size() - cbtextlen);
cbtext.append(QString("Ride data read: %1:%2").arg(min / 60)
.arg(min % 60, 2, 10, QLatin1Char('0')));
}
if (!statusCallback(cbtext)) {
err = "download cancelled";
return false;
}
if (!doWrite(dev, 0x71, hwecho, err)) // 'q'
return false;
}
QString tmpl = tmpdir.absoluteFilePath(".ptdl.XXXXXX");
QTemporaryFile tmp(tmpl);
tmp.setAutoRemove(false);
if (!tmp.open()) {
err = "Failed to create temporary file "
+ tmpl + ": " + tmp.error();
return false;
}
QTextStream os(&tmp);
os << hex;
os.setPadChar('0');
struct tm time;
bool time_set = false;
unsigned char *data = records.data();
bool bIsVer81 = PowerTapUtil::is_Ver81(data);
for (int i = 0; i < records.size(); i += 6) {
if (data[i] == 0 && !bIsVer81)
continue;
for (int j = 0; j < 6; ++j) {
os.setFieldWidth(2);
os << data[i+j];
os.setFieldWidth(1);
os << ((j == 5) ? "\n" : " ");
}
if (!time_set && PowerTapUtil::is_time(data + i, bIsVer81)) {
PowerTapUtil::unpack_time(data + i, &time, bIsVer81);
time_set = true;
}
}
if (!time_set) {
err = "Failed to find ride time.";
tmp.setAutoRemove(true);
return false;
}
tmpname = tmp.fileName(); // after close(), tmp.fileName() is ""
tmp.close();
// QTemporaryFile initially has permissions set to 0600.
// Make it readable by everyone.
tmp.setPermissions(tmp.permissions()
| QFile::ReadOwner | QFile::ReadUser
| QFile::ReadGroup | QFile::ReadOther);
char filename_tmp[32];
sprintf(filename_tmp, "%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);
filename = filename_tmp;
return true;
}

34
src/PowerTapDevice.h Normal file
View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2008 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_PowerTapDevice_h
#define _GC_PowerTapDevice_h 1
#include "CommPort.h"
#include "Device.h"
struct PowerTapDevice : public Device
{
virtual QString downloadInstructions() const;
virtual bool download(CommPortPtr dev, const QDir &tmpdir,
QString &tmpname, QString &filename,
StatusCallback statusCallback, QString &err);
};
#endif // _GC_PowerTapDevice_h

219
src/PowerTapUtil.cpp Normal file
View File

@@ -0,0 +1,219 @@
/*
* Copyright (c) 2008 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 "PowerTapUtil.h"
#include <QString>
#include <math.h>
bool
PowerTapUtil::is_ignore_record(unsigned char *buf, bool bVer81)
{
if (bVer81)
return buf[0]==0 && buf[1]==0 && buf[2]==0;
else
return buf[0]==0;
}
bool
PowerTapUtil::is_Ver81(unsigned char *buf)
{
return buf[3] == 0x81;
}
int
PowerTapUtil::is_time(unsigned char *buf, bool bVer81)
{
return (bVer81 && buf[0] == 0x10) || (!bVer81 && buf[0] == 0x60);
}
time_t
PowerTapUtil::unpack_time(unsigned char *buf, struct tm *time, bool bVer81)
{
(void) bVer81; // unused
memset(time, 0, sizeof(*time));
time->tm_year = 2000 + buf[1] - 1900;
time->tm_mon = buf[2] - 1;
time->tm_mday = buf[3] & 0x1f;
time->tm_hour = buf[4] & 0x1f;
time->tm_min = buf[5] & 0x3f;
time->tm_sec = ((buf[3] >> 5) << 3) | (buf[4] >> 5);
time->tm_isdst = -1;
return mktime(time);
}
int
PowerTapUtil::is_config(unsigned char *buf, bool bVer81)
{
return (bVer81 && buf[0] == 0x00) || (!bVer81 && buf[0] == 0x40);
}
const double TIME_UNIT_SEC = 0.021*60.0;
const double TIME_UNIT_SEC_V81 = 0.01;
int
PowerTapUtil::unpack_config(unsigned char *buf, unsigned *interval,
unsigned *last_interval, double *rec_int_secs,
unsigned *wheel_sz_mm, bool bVer81)
{
*wheel_sz_mm = (buf[1] << 8) | buf[2];
/* Data from device wraps interval after 9... */
if (buf[3] != *last_interval) {
*last_interval = buf[3];
++*interval;
}
*rec_int_secs = buf[4];
if (bVer81)
{
*rec_int_secs *= TIME_UNIT_SEC_V81;
}
else
{
*rec_int_secs += 1;
*rec_int_secs *= TIME_UNIT_SEC;
}
return 0;
}
int
PowerTapUtil::is_data(unsigned char *buf, bool bVer81)
{
if (bVer81)
return (buf[0] & 0x40) == 0x40;
else
return (buf[0] & 0x80) == 0x80;
}
static double
my_round(double x)
{
int i = (int) x;
double z = x - i;
/* For some unknown reason, the PowerTap software rounds 196.5 down... */
if ((z > 0.5) || ((z == 0.5) && (i != 196)))
++i;
return i;
}
#define MAGIC_CONSTANT 147375.0
#define PI M_PI
#define LBFIN_TO_NM 0.11298483
#define KM_TO_MI 0.62137119
#define BAD_LBFIN_TO_NM_1 0.112984
#define BAD_LBFIN_TO_NM_2 0.1129824
#define BAD_KM_TO_MI 0.62
void
PowerTapUtil::unpack_data(unsigned char *buf, int compat, double rec_int_secs,
unsigned wheel_sz_mm, double *time_secs,
double *torque_Nm, double *mph, double *watts,
double *dist_m, unsigned *cad, unsigned *hr,
bool bVer81)
{
if (bVer81)
{
const double CLOCK_TICK_TIME = 0.000512;
const double METERS_PER_SEC_TO_MPH = 2.23693629;
*time_secs += rec_int_secs;
int rotations = buf[0] & 0x0f;
int ticks_for_1_rotation = (buf[1]<<4) | (buf[2]>>4);
if (ticks_for_1_rotation==0xff0 || ticks_for_1_rotation==0)
{
*watts = 0;
*cad = 0;
*mph = 0;
*torque_Nm = 0;
}
else
{
*watts = ((buf[2] & 0x0f) << 8) | buf[3];
*cad = buf[4];
if (*cad == 0xff)
*cad = 0;
double wheel_sz_meters = wheel_sz_mm / 1000.0;
*dist_m += rotations * wheel_sz_meters;
double seconds_for_1_rotation = ticks_for_1_rotation * CLOCK_TICK_TIME;
double meters_per_sec = wheel_sz_meters / seconds_for_1_rotation;
*mph = meters_per_sec * METERS_PER_SEC_TO_MPH;
*torque_Nm = (*watts * seconds_for_1_rotation)/(2.0*PI);
}
*hr = buf[5];
if (*hr == 0xff)
*hr = 0;
}
else
{
double kph10;
unsigned speed;
unsigned torque_inlbs;
*time_secs += rec_int_secs;
torque_inlbs = ((buf[1] & 0xf0) << 4) | buf[2];
if (torque_inlbs == 0xfff)
torque_inlbs = 0;
speed = ((buf[1] & 0x0f) << 8) | buf[3];
if ((speed < 100) || (speed == 0xfff)) {
if ((speed != 0) && (speed < 1000)) {
fprintf(stderr, "possible error: speed=%.1f; ignoring it\n",
MAGIC_CONSTANT / speed / 10.0);
}
*mph = -1.0;
*watts = -1.0;
}
else {
if (compat)
*torque_Nm = torque_inlbs * BAD_LBFIN_TO_NM_2;
else
*torque_Nm = torque_inlbs * LBFIN_TO_NM;
kph10 = MAGIC_CONSTANT / speed;
if (compat)
*mph = my_round(kph10) / 10.0 * BAD_KM_TO_MI;
else
*mph = kph10 / 10.0 * KM_TO_MI;
// from http://en.wikipedia.org/wiki/Torque#Conversion_to_other_units
double dMetersPerMinute = (kph10 / 10.0) * 1000.0 / 60.0;
double dWheelSizeMeters = wheel_sz_mm / 1000.0;
double rpm = dMetersPerMinute/dWheelSizeMeters;
*watts = *torque_Nm * rpm * 2.0 * PI /60.0;
if (compat)
*watts = my_round(*watts);
else
*watts = round(*watts);
}
if (compat)
*torque_Nm = torque_inlbs * BAD_LBFIN_TO_NM_1;
*dist_m += (buf[0] & 0x7f) * wheel_sz_mm / 1000.0;
*cad = buf[4];
if (*cad == 0xff)
*cad = 0;
*hr = buf[5];
if (*hr == 0xff)
*hr = 0;
}
}

46
src/PowerTapUtil.h Normal file
View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2008 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_PowerTapUtil_h
#define _GC_PowerTapUtil_h 1
#include <time.h>
struct PowerTapUtil
{
static bool is_Ver81(unsigned char *bufHeader);
static bool is_ignore_record(unsigned char *buf, bool bVer81);
static int is_time(unsigned char *buf, bool bVer81);
static time_t unpack_time(unsigned char *buf, struct tm *time, bool bVer81);
static int is_config(unsigned char *buf, bool bVer81);
static int unpack_config(unsigned char *buf, unsigned *interval,
unsigned *last_interval, double *rec_int_secs,
unsigned *wheel_sz_mm, bool bVer81);
static int is_data(unsigned char *buf, bool bVer81);
static void unpack_data(unsigned char *buf, int compat, double rec_int_secs,
unsigned wheel_sz_mm, double *time_secs,
double *torque_Nm, double *mph, double *watts,
double *dist_m, unsigned *cad, unsigned *hr, bool bVer81);
};
#endif // _GC_PowerTapUtil_h

128
src/QuarqParser.cpp Normal file
View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
*
* 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 <QString>
#include <iostream>
#include <assert.h>
#include "QuarqParser.h"
QuarqParser::QuarqParser (RideFile* rideFile)
: rideFile(rideFile),
version(""),
km(0),
watts(0),
cad(0),
hr(0),
initial_seconds(-1.0),
seconds_from_start(0),
kph(0),
nm(0)
{
;
}
// implement sample-and-hold resampling
#define SAMPLE_INTERVAL 1.0 // seconds
void
QuarqParser::incrementTime( const double new_time )
{
if (initial_seconds < 0.0) {
initial_seconds = new_time;
}
float time_diff = new_time - initial_seconds;
while (time_diff > seconds_from_start) {
rideFile->appendPoint(seconds_from_start, cad, hr, km,
kph, nm, watts, 0, 9, 0);
seconds_from_start += SAMPLE_INTERVAL;
}
time.setTime_t(new_time);
}
bool
QuarqParser::startElement( const QString&, const QString&,
const QString& qName,
const QXmlAttributes& qAttributes)
{
buf.clear();
if (qName == "Qollector") {
version = qAttributes.value("version");
// reset the timer for a new <Qollector> tag
seconds_from_start = 0.0;
initial_seconds = -1;
return TRUE;
}
#define CheckQuarqXml(name,unit,dest) do { \
if (qName== #name) { \
QString name = qAttributes.value( #unit ); \
QString timestamp = qAttributes.value("timestamp"); \
\
if ((! name.isEmpty()) && (!timestamp.isEmpty()) && \
( name.toLower() != "nan")) { \
dest = name.toDouble(); \
incrementTime(timestamp.toDouble()); \
} \
return TRUE; \
} \
} while (0);
CheckQuarqXml(Cadence, RPM, cad );
CheckQuarqXml(Power, Watts, watts );
CheckQuarqXml(HeartRate, BPM, hr );
// clearly bogus, equating RPM to kph.
// Unless you have an 18 foot wheel, by chance
CheckQuarqXml(Speed, RPM, kph );
#undef CheckQuarqXml
// default case
// only print the first time and unknown happens
if (!unknown_keys[qName]++)
std::cerr << "Unknown Element " << qPrintable(qName) << std::endl;
return TRUE;
}
bool
QuarqParser::endElement( const QString&, const QString&, const QString& qName)
{
// flush one last data point
if (qName == "Qollector") {
rideFile->appendPoint(seconds_from_start, cad, hr, km,
kph, nm, watts, 0, 0, 0);
}
return TRUE;
}
bool
QuarqParser::characters( const QString& str )
{
buf += str;
return TRUE;
}

68
src/QuarqParser.h Normal file
View File

@@ -0,0 +1,68 @@
/*
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
*
* 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 _QuarqParser_h
#define _QuarqParser_h
#include "RideFile.h"
#include <QString>
#include <QHash>
#include <QDateTime>
#include <QProcess>
#include <QXmlDefaultHandler>
class QuarqParser : public QXmlDefaultHandler
{
public:
QuarqParser(RideFile* rideFile);
bool startElement( const QString&, const QString&, const QString&,
const QXmlAttributes& );
bool endElement( const QString&, const QString&, const QString& );
bool characters( const QString& );
private:
void incrementTime( const double new_time ) ;
RideFile* rideFile;
QString buf;
QString version;
QDateTime time;
double km;
double watts;
double cad;
double hr;
double initial_seconds;
double seconds_from_start;
double kph;
double nm;
QHash<QString, int> unknown_keys;
};
#endif // _QuarqParser_h

153
src/QuarqRideFile.cpp Normal file
View File

@@ -0,0 +1,153 @@
/*
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
*
* 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 "QuarqRideFile.h"
#include "QuarqParser.h"
#include <iostream>
#include <assert.h>
static QString installed_path = "";
QProcess *getInterpreterProcess( QString path ) {
QProcess *antProcess;
antProcess = new QProcess( );
antProcess->start( path );
if (!antProcess->waitForStarted()) {
delete antProcess;
antProcess = NULL;
}
return antProcess;
}
/*
Thanks to ANT+ nondisclosure agreements, the Quarq ANT+
interpretation code lives in a closed source binary. It takes the
log on stdin and writes XML to stdout.
If the binary is not available, no Quarq ANT+ capability is shown
in the menu, nor are any ANT+ files opened.
QProcess note:
It turns out that the interpreter must actually be opened and
executed before we can be sure it's there. Checking the return
value of start() isn't sufficient. On my Linux system, start()
returns true upon opening the OS X build of qollector_interpret.
*/
bool quarqInterpreterInstalled( void ) {
static bool checkedInstallation = false;
static bool installed;
if (!checkedInstallation) {
QString interpreterPath="/usr/local/bin/qollector_interpret/build-";
QStringList executables;
executables << "linux-i386/qollector_interpret";
executables << "osx-ppc-i386/qollector_interpret";
executables << "win32/qollector_interpret.exe";
for ( QStringList::Iterator ex = executables.begin(); ex != executables.end(); ++ex ) {
QProcess *antProcess = getInterpreterProcess( interpreterPath + *ex );
if (NULL == antProcess) {
installed = false;
} else {
antProcess->closeWriteChannel();
antProcess->waitForFinished(-1);
installed=((QProcess::NormalExit == antProcess->exitStatus()) &&
(0 == antProcess->exitCode()));
delete antProcess;
if (installed) {
installed_path = interpreterPath + *ex;
break;
}
}
}
if (!installed)
std::cerr << "Cannot open qollector_interpret program, available from http://opensource.quarq.us/qollector_interpret." << std::endl;
checkedInstallation = true;
}
return installed;
}
static int antFileReaderRegistered =
quarqInterpreterInstalled() ? RideFileFactory::instance().registerReader("qla", new QuarqFileReader()) : 0;
RideFile *QuarqFileReader::openRideFile(QFile &file, QStringList &errors) const
{
(void) errors;
RideFile *rideFile = new RideFile();
rideFile->setDeviceType("Quarq Qollector");
QuarqParser handler(rideFile);
QProcess *antProcess = getInterpreterProcess( installed_path );
assert(antProcess);
QXmlInputSource source (antProcess);
QXmlSimpleReader reader;
reader.setContentHandler (&handler);
// this could done be a loop to "save memory."
file.open(QIODevice::ReadOnly);
antProcess->write(file.readAll());
antProcess->closeWriteChannel();
antProcess->waitForFinished(-1);
assert(QProcess::NormalExit == antProcess->exitStatus());
assert(0 == antProcess->exitCode());
reader.parse(source);
reader.parseContinue();
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.qla$");
if (rideTime.indexIn(file.fileName()) >= 0) {
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
rideTime.cap(2).toInt(),
rideTime.cap(3).toInt()),
QTime(rideTime.cap(4).toInt(),
rideTime.cap(5).toInt(),
rideTime.cap(6).toInt()));
rideFile->setStartTime(datetime);
}
delete antProcess;
return rideFile;
}

32
src/QuarqRideFile.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
*
* 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 _QuarqRideFile_h
#define _QuarqRideFile_h
#include "RideFile.h"
#include <QProcess>
bool quarqInterpreterInstalled( void );
struct QuarqFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _QuarqRideFile_h

5
src/README.txt Normal file
View File

@@ -0,0 +1,5 @@
To uninstall the older FTDI VCP drivers on Mac OS X, open a Terminal and type:
sudo mv /System/Library/Extensions/FTDIUSBSerialDriver.kext /tmp
Type your password when prompted, then restart your computer.

217
src/RawRideFile.cpp Normal file
View File

@@ -0,0 +1,217 @@
/*
* Copyright (c) 2007-2008 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 "RawRideFile.h"
#include "PowerTapUtil.h"
#include <assert.h>
#include <math.h>
#define MILES_TO_KM 1.609344
#define KM_TO_MI 0.62137119
#define BAD_KM_TO_MI 0.62
static int rawFileReaderRegistered =
RideFileFactory::instance().registerReader("raw", new RawFileReader());
struct ReadState
{
RideFile *rideFile;
QStringList &errors;
double last_secs, last_miles;
unsigned last_interval;
time_t start_since_epoch;
// this seems to not be used
//unsigned rec_int;
ReadState(RideFile *rideFile,
QStringList &errors) :
rideFile(rideFile), 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, double rec_int_secs,
unsigned wheel_sz_mm, void *context)
{
(void) interval;
(void) wheel_sz_mm;
ReadState *state = (ReadState*) context;
// Assume once set, rec_int should never change.
//double recIntSecs = rec_int * 1.26;
assert((state->rideFile->recIntSecs() == 0.0)
|| (state->rideFile->recIntSecs() == rec_int_secs));
state->rideFile->setRecIntSecs(rec_int_secs);
}
static void
time_cb(struct tm *, time_t since_epoch, void *context)
{
ReadState *state = (ReadState*) context;
if (state->rideFile->startTime().isNull())
{
QDateTime t;
t.setTime_t(since_epoch);
state->rideFile->setStartTime(t);
}
if (state->start_since_epoch == 0)
state->start_since_epoch = since_epoch;
double secs = since_epoch - state->start_since_epoch;
state->rideFile->appendPoint(secs, 0.0, 0.0,
state->last_miles * MILES_TO_KM, 0.0,
0.0, 0.0, 0.0, state->last_interval);
state->last_secs = secs;
}
static void
data_cb(double secs, double nm, double mph, double watts, double miles, double alt,
unsigned cad, unsigned hr, unsigned interval, void *context)
{
if (nm < 0.0) nm = 0.0;
if (mph < 0.0) mph = 0.0;
if (watts < 0.0) watts = 0.0;
ReadState *state = (ReadState*) context;
state->rideFile->appendPoint(secs, cad, hr, miles * MILES_TO_KM,
mph * MILES_TO_KM, nm, watts, alt, interval);
state->last_secs = secs;
state->last_miles = miles;
state->last_interval = interval;
}
static void
error_cb(const char *msg, void *context)
{
ReadState *state = (ReadState*) context;
state->errors.append(QString(msg));
}
static void
pt_read_raw(FILE *in, int compat, void *context,
void (*config_cb)(unsigned interval, double rec_int_secs,
unsigned wheel_sz_mm, void *context),
void (*time_cb)(struct tm *time, time_t since_epoch, void *context),
void (*data_cb)(double secs, double nm, double mph, double watts,
double miles, double alt, unsigned cad, unsigned hr,
unsigned interval, void *context),
void (*error_cb)(const char *msg, void *context))
{
unsigned interval = 0;
unsigned last_interval = 0;
unsigned wheel_sz_mm = 0;
double rec_int_secs = 0.0;
int i, n, row = 0;
unsigned char buf[6];
unsigned sbuf[6];
double meters = 0.0;
double secs = 0.0, start_secs = 0.0;
double miles;
double mph;
double nm;
double watts;
double alt;
unsigned cad;
unsigned hr;
struct tm time;
time_t since_epoch;
char ebuf[256];
bool bIsVer81 = false;
while ((n = fscanf(in, "%x %x %x %x %x %x\n",
sbuf, sbuf+1, sbuf+2, sbuf+3, sbuf+4, sbuf+5)) == 6) {
++row;
for (i = 0; i < 6; ++i) {
if (sbuf[i] > 0xff) { n = 1; break; }
buf[i] = sbuf[i];
}
if (row == 1)
{
/* Serial number? */
bIsVer81 = PowerTapUtil::is_Ver81(buf);
}
else if (PowerTapUtil::is_ignore_record(buf, bIsVer81)) {
// do nothing
}
else if (PowerTapUtil::is_config(buf, bIsVer81)) {
if (PowerTapUtil::unpack_config(buf, &interval, &last_interval,
&rec_int_secs, &wheel_sz_mm, bIsVer81) < 0) {
sprintf(ebuf, "Couldn't unpack config record.");
if (error_cb) error_cb(ebuf, context);
return;
}
if (config_cb) config_cb(interval, rec_int_secs, wheel_sz_mm, context);
}
else if (PowerTapUtil::is_time(buf, bIsVer81)) {
since_epoch = PowerTapUtil::unpack_time(buf, &time, bIsVer81);
bool ignore = false;
if (start_secs == 0.0)
start_secs = since_epoch;
else if (since_epoch - start_secs > secs)
secs = since_epoch - start_secs;
else {
sprintf(ebuf, "Warning: %0.3f minutes into the ride, "
"time jumps backwards by %0.3f minutes; ignoring it.",
secs / 60.0, (secs - since_epoch + start_secs) / 60.0);
if (error_cb) error_cb(ebuf, context);
ignore = true;
}
if (time_cb && !ignore) time_cb(&time, since_epoch, context);
}
else if (PowerTapUtil::is_data(buf, bIsVer81)) {
if (wheel_sz_mm == 0) {
sprintf(ebuf, "Read data row before wheel size set.");
if (error_cb) error_cb(ebuf, context);
return;
}
PowerTapUtil::unpack_data(buf, compat, rec_int_secs, wheel_sz_mm, &secs,
&nm, &mph, &watts, &meters, &cad, &hr, bIsVer81);
if (compat)
miles = round(meters) / 1000.0 * BAD_KM_TO_MI;
else
miles = meters / 1000.0 * KM_TO_MI;
if (data_cb)
data_cb(secs, nm, mph, watts, miles, alt, cad,
hr, interval, context);
}
else {
sprintf(ebuf, "Unknown record type 0x%x on row %d.", buf[0], row);
if (error_cb) error_cb(ebuf, context);
return;
}
}
if (n != -1) {
sprintf(ebuf, "Parse error on row %d.", row);
if (error_cb) error_cb(ebuf, context);
return;
}
}
RideFile *RawFileReader::openRideFile(QFile &file, QStringList &errors) const
{
RideFile *rideFile = new RideFile;
rideFile->setDeviceType("PowerTap");
if (!file.open(QIODevice::ReadOnly)) {
delete rideFile;
return NULL;
}
FILE *f = fdopen(file.handle(), "r");
assert(f);
ReadState state(rideFile, errors);
pt_read_raw(f, 0 /* not compat */, &state, config_cb,
time_cb, data_cb, error_cb);
return rideFile;
}

29
src/RawRideFile.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2007 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 _RawRideFile_h
#define _RawRideFile_h
#include "RideFile.h"
struct RawFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _RawRideFile_h

141
src/RideCalendar.cpp Normal file
View File

@@ -0,0 +1,141 @@
#include <QCalendarWidget>
#include <QMultiMap>
#include <QPainter>
#include <QObject>
#include <QDate>
#include <QAbstractItemView>
#include <QSize>
#include <QTextCharFormat>
#include <QPen>
#include "RideItem.h"
#include "RideCalendar.h"
RideCalendar::RideCalendar(QWidget *parent)
: QCalendarWidget(parent)
{
};
void RideCalendar::paintCell(QPainter *painter, const QRect &rect, const QDate &date) const
{
if (_text.contains(date)) {
painter->save();
/*
* Draw a rectangle in the color specified. If this is the
* currently selected date, draw a black outline.
*/
QPen pen(Qt::SolidLine);
pen.setCapStyle(Qt::SquareCap);
painter->setBrush(_color[date]);
if (date == selectedDate()) {
pen.setColor(Qt::black);
pen.setWidth(1);
} else {
pen.setColor(_color[date]);
}
painter->setPen(pen);
/*
* We have to draw to height-1 and width-1 because Qt draws outlines
* outside the box by default.
*/
painter->drawRect(rect.x(), rect.y(), rect.width() - 1, rect.height() - 1);
/*
* Display the text.
*/
pen.setColor(Qt::black);
painter->setPen(pen);
QString text = QString::number(date.day());
text = text + "\n" + _text[date];
QFont font = painter->font();
font.setPointSize(font.pointSize() - 2);
painter->setFont(font);
painter->drawText(rect, Qt::AlignHCenter | Qt::TextWordWrap, text);
painter->restore();
} else {
QCalendarWidget::paintCell(painter, rect, date);
}
}
void RideCalendar::setHome(const QDir &homeDir)
{
home = homeDir;
}
void RideCalendar::addRide(RideItem* ride)
{
/*
* We want to display these things inside the Calendar.
* Pick a colour (this should really be configurable)
* - red for races
* - yellow for sick days
* - green for rides
*/
QDateTime dt = ride->dateTime;
QString notesPath = home.absolutePath() + "/" + ride->notesFileName;
QFile notesFile(notesPath);
QColor color(Qt::green);
QString line("Ride");
QString code;
if (notesFile.exists()) {
if (notesFile.open(QFile::ReadOnly | QFile::Text)) {
QTextStream in(&notesFile);
line = in.readLine();
notesFile.close();
foreach(code, workoutCodes.keys()) {
if (line.contains(code, Qt::CaseInsensitive)) {
color = workoutCodes[code];
}
}
}
}
addEvent(dt.date(), line, color);
}
void RideCalendar::removeRide(RideItem* ride)
{
removeEvent(ride->dateTime.date());
}
void RideCalendar::addWorkoutCode(QString string, QColor color)
{
workoutCodes[string] = color;
}
/*
* Private:
* Add a string, and a color, to a specific date.
*/
void RideCalendar::addEvent(QDate date, QString string, QColor color)
{
_text[date] = string;
_color[date] = color;
update();
}
/*
* Private:
* Remove the info for a current date.
*/
void RideCalendar::removeEvent(QDate date)
{
if (_text.contains(date)) {
_text.remove(date);
_color.remove(date);
}
}
/*
* We extend QT's QCalendarWidget's sizeHint() so we claim a little bit of
* extra space.
*/
QSize RideCalendar::sizeHint() const
{
QSize hint = QCalendarWidget::sizeHint();
hint.setHeight(hint.height() * 2);
hint.setWidth(hint.width() * 2);
return hint;
}

32
src/RideCalendar.h Normal file
View File

@@ -0,0 +1,32 @@
#ifndef EVENT_CALENDAR_WIDGET_H
#define EVENT_CALENDAR_WIDGET_H
#include <QCalendarWidget>
#include <QMultiMap>
#include "RideItem.h"
class RideCalendar : public QCalendarWidget
{
Q_OBJECT
public:
RideCalendar(QWidget *parent = 0);
void removeRide(RideItem*);
void addRide(RideItem*);
QSize sizeHint() const;
void setHome(const QDir&);
void addWorkoutCode(QString, QColor);
protected:
void paintCell(QPainter *, const QRect &, const QDate &) const;
private:
void addEvent(QDate, QString, QColor);
void removeEvent(QDate);
QMap<QDate, QString> _text;
QMap<QDate, QColor> _color;
QMap<QString, QColor> workoutCodes;
QDir home;
};
#endif

219
src/RideFile.cpp Normal file
View File

@@ -0,0 +1,219 @@
/*
* Copyright (c) 2007 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 "RideFile.h"
#include <QtXml/QtXml>
#include <assert.h>
#include "Settings.h"
static void
markInterval(QDomDocument &doc, QDomNode &xride, QDomNode &xintervals,
double &startSecs, double prevSecs,
int &thisInterval, RideFilePoint *sample)
{
if (xintervals.isNull()) {
xintervals = doc.createElement("intervals");
xride.appendChild(xintervals);
}
QDomElement xint = doc.createElement("interval").toElement();
xintervals.appendChild(xint);
xint.setAttribute("name", thisInterval);
xint.setAttribute("from_secs", QString("%1").arg(startSecs, 0, 'f', 2));
xint.setAttribute("thru_secs", QString("%1").arg(prevSecs, 0, 'f', 2));
startSecs = sample->secs;
thisInterval = sample->interval;
}
static void
append_text(QDomDocument &doc, QDomNode &parent,
const QString &child_name, const QString &child_value)
{
QDomNode child = parent.appendChild(doc.createElement(child_name));
child.appendChild(doc.createTextNode(child_value));
}
bool
RideFile::writeAsXml(QFile &file, QString &err) const
{
(void) err;
QDomDocument doc("GoldenCheetah-1.0");
QDomNode xride = doc.appendChild(doc.createElement("ride"));
QDomNode xheader = xride.appendChild(doc.createElement("header"));
append_text(doc, xheader, "start_time", startTime_.toString("yyyy/MM/dd hh:mm:ss"));
append_text(doc, xheader, "device_type", deviceType_);
append_text(doc, xheader, "rec_int_secs", QString("%1").arg(recIntSecs_, 0, 'f', 3));
QDomNode xintervals;
bool hasNm = false;
double startSecs = 0.0, prevSecs = 0.0;
int thisInterval = 0;
QListIterator<RideFilePoint*> i(dataPoints_);
RideFilePoint *sample = NULL;
while (i.hasNext()) {
sample = i.next();
if (sample->nm > 0.0)
hasNm = true;
assert(sample->secs >= 0.0);
if (sample->interval != thisInterval) {
markInterval(doc, xride, xintervals, startSecs,
prevSecs, thisInterval, sample);
}
prevSecs = sample->secs;
}
if (sample) {
markInterval(doc, xride, xintervals, startSecs,
prevSecs, thisInterval, sample);
}
QDomNode xsamples = doc.createElement("samples");
xride.appendChild(xsamples);
i.toFront();
while (i.hasNext()) {
RideFilePoint *sample = i.next();
QDomElement xsamp = doc.createElement("sample").toElement();
xsamples.appendChild(xsamp);
xsamp.setAttribute("secs", QString("%1").arg(sample->secs, 0, 'f', 2));
xsamp.setAttribute("cad", QString("%1").arg(sample->cad, 0, 'f', 0));
xsamp.setAttribute("hr", QString("%1").arg(sample->hr, 0, 'f', 0));
xsamp.setAttribute("km", QString("%1").arg(sample->km, 0, 'f', 3));
xsamp.setAttribute("kph", QString("%1").arg(sample->kph, 0, 'f', 1));
xsamp.setAttribute("alt", QString("%1").arg(sample->alt, 0, 'f', 1));
xsamp.setAttribute("watts", sample->watts);
if (hasNm) {
double nm = (sample->watts > 0.0) ? sample->nm : 0.0;
xsamp.setAttribute("nm", QString("%1").arg(nm, 0,'f', 1));
}
}
file.open(QFile::WriteOnly);
QTextStream ts(&file);
doc.save(ts, 4);
file.close();
return true;
}
void RideFile::writeAsCsv(QFile &file, bool bIsMetric) const
{
// Use the column headers that make WKO+ happy.
double convertUnit;
QTextStream out(&file);
if (!bIsMetric)
{
out << "Minutes,Torq (N-m),MPH,Watts,Miles,Cadence,Hrate,ID,Altitude (feet)\n";
const double MILES_PER_KM = 0.62137119;
convertUnit = MILES_PER_KM;
}
else {
out << "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)\n";
// TODO: use KM_TO_MI from lib/pt.c instead?
convertUnit = 1.0;
}
QListIterator<RideFilePoint*> i(dataPoints());
while (i.hasNext()) {
RideFilePoint *point = i.next();
if (point->secs == 0.0)
continue;
out << point->secs/60.0;
out << ",";
out << ((point->nm >= 0) ? point->nm : 0.0);
out << ",";
out << ((point->kph >= 0) ? (point->kph * convertUnit) : 0.0);
out << ",";
out << ((point->watts >= 0) ? point->watts : 0.0);
out << ",";
out << point->km * convertUnit;
out << ",";
out << point->cad;
out << ",";
out << point->hr;
out << ",";
out << point->interval;
out << ",";
out << point->alt;
if (point->bs > 0.0) {
out << ",";
out << point->bs;
}
out << "\n";
}
file.close();
}
RideFileFactory *RideFileFactory::instance_;
RideFileFactory &RideFileFactory::instance()
{
if (!instance_)
instance_ = new RideFileFactory();
return *instance_;
}
int RideFileFactory::registerReader(const QString &suffix,
RideFileReader *reader)
{
assert(!readFuncs_.contains(suffix));
readFuncs_.insert(suffix, reader);
return 1;
}
RideFile *RideFileFactory::openRideFile(QFile &file,
QStringList &errors) const
{
QString suffix = file.fileName();
int dot = suffix.lastIndexOf(".");
assert(dot >= 0);
suffix.remove(0, dot + 1);
RideFileReader *reader = readFuncs_.value(suffix.toLower());
assert(reader);
return reader->openRideFile(file, errors);
}
QStringList RideFileFactory::listRideFiles(const QDir &dir) const
{
QStringList filters;
QMapIterator<QString,RideFileReader*> i(readFuncs_);
while (i.hasNext()) {
i.next();
filters << ("*." + i.key());
}
// This will read the user preferences and change the file list order as necessary:
boost::shared_ptr<QSettings> settings = GetApplicationSettings();;
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked);
if(isAscending.toInt()>0){
return dir.entryList(filters, QDir::Files, QDir::Name);
}
return dir.entryList(filters, QDir::Files, QDir::Name|QDir::Reversed);
}
void RideFile::appendPoint(double secs, double cad, double hr, double km,
double kph, double nm, double watts, double alt,
int interval, double bs)
{
dataPoints_.append(new RideFilePoint(secs, cad, hr, km, kph,
nm, watts, alt, interval,bs));
dataPresent.secs |= (secs != 0);
dataPresent.cad |= (cad != 0);
dataPresent.hr |= (hr != 0);
dataPresent.km |= (km != 0);
dataPresent.kph |= (kph != 0);
dataPresent.nm |= (nm != 0);
dataPresent.watts |= (watts != 0);
dataPresent.alt |= (alt != 0);
dataPresent.interval |= (interval != 0);
}

131
src/RideFile.h Normal file
View File

@@ -0,0 +1,131 @@
/*
* Copyright (c) 2007 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 _RideFile_h
#define _RideFile_h
#include <QDate>
#include <QDir>
#include <QFile>
#include <QList>
#include <QMap>
// This file defines four classes:
//
// RideFile, as the name suggests, represents the data stored in a ride file,
// regardless of what type of file it is (.raw, .srm, .csv).
//
// RideFilePoint represents the data for a single sample in a RideFile.
//
// RideFileReader is an abstract base class for function-objects that take a
// filename and return a RideFile object representing the ride stored in the
// corresponding file.
//
// RideFileFactory is a singleton that maintains a mapping from ride file
// suffixes to the RideFileReader objects capable of converting those files
// into RideFile objects.
struct RideFilePoint
{
double secs, cad, hr, km, kph, nm, watts, alt;
int interval;
double bs; // to init in order
RideFilePoint() : secs(0.0), cad(0.0), hr(0.0), km(0.0), kph(0.0),
nm(0.0), watts(0.0), alt(0.0), interval(0), bs(0.0) {}
RideFilePoint(double secs, double cad, double hr, double km, double kph,
double nm, double watts, double alt, int interval, double bs) :
secs(secs), cad(cad), hr(hr), km(km), kph(kph), nm(nm),
watts(watts), alt(alt), interval(interval), bs(bs) {}
};
struct RideFileDataPresent
{
bool secs, cad, hr, km, kph, nm, watts, alt, interval;
// whether non-zero data of each field is present
RideFileDataPresent():
secs(false), cad(false), hr(false), km(false),
kph(false), nm(false), watts(false), alt(false), interval(false) {}
};
class RideFile
{
private:
QDateTime startTime_; // time of day that the ride started
double recIntSecs_; // recording interval in seconds
QList<RideFilePoint*> dataPoints_;
RideFileDataPresent dataPresent;
QString deviceType_;
public:
RideFile() : recIntSecs_(0.0), deviceType_("unknown") {}
RideFile(const QDateTime &startTime, double recIntSecs) :
startTime_(startTime), recIntSecs_(recIntSecs),
deviceType_("unknown") {}
virtual ~RideFile() {
QListIterator<RideFilePoint*> i(dataPoints_);
while (i.hasNext())
delete i.next();
}
const QDateTime &startTime() const { return startTime_; }
double recIntSecs() const { return recIntSecs_; }
const QList<RideFilePoint*> dataPoints() const { return dataPoints_; }
inline RideFileDataPresent *areDataPresent() { return &dataPresent; }
const QString &deviceType() const { return deviceType_; }
void setStartTime(const QDateTime &value) { startTime_ = value; }
void setRecIntSecs(double value) { recIntSecs_ = value; }
void setDeviceType(const QString &value) { deviceType_ = value; }
void appendPoint(double secs, double cad, double hr, double km,
double kph, double nm, double watts, double alt, int interval, double bs=0.0);
bool writeAsXml(QFile &file, QString &err) const;
void writeAsCsv(QFile &file, bool bIsMetric) const;
void resetDataPresent();
};
struct RideFileReader {
virtual ~RideFileReader() {}
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const = 0;
};
class RideFileFactory {
private:
static RideFileFactory *instance_;
QMap<QString,RideFileReader*> readFuncs_;
RideFileFactory() {}
public:
static RideFileFactory &instance();
int registerReader(const QString &suffix, RideFileReader *reader);
RideFile *openRideFile(QFile &file, QStringList &errors) const;
QStringList listRideFiles(const QDir &dir) const;
};
#endif // _RideFile_h

446
src/RideItem.cpp Normal file
View File

@@ -0,0 +1,446 @@
/*
* 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 "RideMetric.h"
#include "RideFile.h"
#include "Settings.h"
#include "TimeUtils.h"
#include "Zones.h"
#include <iostream> // delete me
#include <assert.h>
#include <math.h>
#include <QtXml/QtXml>
#define MILES_PER_KM 0.62137119
RideItem::RideItem(int type,
QString path, QString fileName, const QDateTime &dateTime,
Zones **zones, QString notesFileName) :
QTreeWidgetItem(type), path(path), fileName(fileName),
dateTime(dateTime), zones(zones), notesFileName(notesFileName)
{
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);
time_in_zone = NULL;
}
RideItem::~RideItem()
{
MetricIter i(metrics);
while (i.hasNext()) {
i.next();
delete i.value();
}
}
static void summarize(QString &intervals,
unsigned last_interval,
double km_start, double km_end,
double &int_watts_sum,
double &int_hr_sum,
double &int_cad_sum,
double &int_kph_sum,
double &int_secs_hr,
double &int_max_power,
double int_dur)
{
double dur = int_dur;
double mile_len = (km_end - km_start) * MILES_PER_KM;
double minutes = (int) (dur/60.0);
double seconds = dur - (60 * minutes);
double watts_avg = int_watts_sum / dur;
double hr_avg = int_hr_sum / int_secs_hr;
double cad_avg = int_cad_sum / dur;
double mph_avg = int_kph_sum * MILES_PER_KM / dur;
double energy = int_watts_sum / 1000.0; // watts_avg / 1000.0 * dur;
intervals += "<tr><td align=\"center\">%1</td>";
intervals += "<td align=\"center\">%2:%3</td>";
intervals += "<td align=\"center\">%4</td>";
intervals += "<td align=\"center\">%5</td>";
intervals += "<td align=\"center\">%6</td>";
intervals += "<td align=\"center\">%7</td>";
intervals += "<td align=\"center\">%8</td>";
intervals += "<td align=\"center\">%9</td>";
intervals += "<td align=\"center\">%10</td>";
intervals = intervals.arg(last_interval);
intervals = intervals.arg(minutes, 0, 'f', 0);
intervals = intervals.arg(seconds, 2, 'f', 0, QLatin1Char('0'));
intervals = intervals.arg(mile_len, 0, 'f', 1);
intervals = intervals.arg(energy, 0, 'f', 0);
intervals = intervals.arg(int_max_power, 0, 'f', 0);
intervals = intervals.arg(watts_avg, 0, 'f', 0);
intervals = intervals.arg(hr_avg, 0, 'f', 0);
intervals = intervals.arg(cad_avg, 0, 'f', 0);
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant unit = settings->value(GC_UNIT);
if(unit.toString() == "Metric")
intervals = intervals.arg(mph_avg * 1.60934, 0, 'f', 1);
else
intervals = intervals.arg(mph_avg, 0, 'f', 1);
int_watts_sum = 0.0;
int_hr_sum = 0.0;
int_cad_sum = 0.0;
int_kph_sum = 0.0;
int_max_power = 0.0;
}
int RideItem::zoneRange()
{
return (
(zones && *zones) ?
(*zones)->whichRange(dateTime.date()) :
-1
);
}
int RideItem::numZones()
{
if (zones && *zones) {
int zone_range = zoneRange();
return ((zone_range >= 0) ?
(*zones)->numZones(zone_range) :
0
);
}
else
return 0;
}
double RideItem::timeInZone(int zone)
{
htmlSummary();
if (!ride)
return 0.0;
assert(zone < numZones());
return time_in_zone[zone];
}
static const char *metricsXml =
"<metrics>\n"
" <metric_group name=\"Totals\">\n"
" <metric name=\"workout_time\" display_name=\"Workout time\"\n"
" precision=\"0\"/>\n"
" <metric name=\"time_riding\" display_name=\"Time riding\"\n"
" precision=\"0\"/>\n"
" <metric name=\"total_distance\" display_name=\"Distance\"\n"
" precision=\"1\"/>\n"
" <metric name=\"total_work\" display_name=\"Work\"\n"
" precision=\"0\"/>\n"
" <metric name=\"elevation_gain\" display_name=\"Elevation Gain\"\n"
" precision=\"1\"/>\n"
" </metric_group>\n"
" <metric_group name=\"Averages\">\n"
" <metric name=\"average_speed\" display_name=\"Speed\"\n"
" precision=\"1\"/>\n"
" <metric name=\"average_power\" display_name=\"Power\"\n"
" precision=\"0\"/>\n"
" <metric name=\"average_hr\" display_name=\"Heart rate\"\n"
" precision=\"0\"/>\n"
" <metric name=\"average_cad\" display_name=\"Cadence\"\n"
" precision=\"0\"/>\n"
" </metric_group>\n"
" <metric_group name=\"BikeScore&#8482;\" note=\"BikeScore is a trademark "
" of Dr. Philip Friere Skiba, PhysFarm Training Systems LLC\">\n"
" <metric name=\"skiba_xpower\" display_name=\"xPower\"\n"
" precision=\"0\"/>\n"
" <metric name=\"skiba_relative_intensity\"\n"
" display_name=\"Relative Intensity\" precision=\"3\"/>\n"
" <metric name=\"skiba_bike_score\" display_name=\"BikeScore\"\n"
" precision=\"0\"/>\n"
" </metric_group>\n"
"</metrics>\n";
QString
RideItem::htmlSummary()
{
if (summary.isEmpty() ||
(zones && *zones && (summaryGenerationTime < (*zones)->modificationTime))) {
// set defaults for zone range and number of zones
int zone_range = -1;
int num_zones = 0;
summaryGenerationTime = QDateTime::currentDateTime();
QFile file(path + "/" + fileName);
QStringList errors;
ride = RideFileFactory::instance().openRideFile(file, errors);
if (!ride) {
summary = "<p>Couldn't read file \"" + file.fileName() + "\":";
QListIterator<QString> i(errors);
while (i.hasNext())
summary += "<br>" + i.next();
return summary;
}
summary = ("<p><center><h2>"
+ dateTime.toString("dddd MMMM d, yyyy, h:mm AP")
+ "</h2><h3>Device Type: " + ride->deviceType() + "</h3>");
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant unit = settings->value(GC_UNIT);
if (zones &&
*zones &&
((zone_range = (*zones)->whichRange(dateTime.date())) >= 0) &&
((num_zones = (*zones)->numZones(zone_range)) > 0)
)
{
time_in_zone = new double[num_zones];
for (int i = 0; i < num_zones; ++i)
time_in_zone[i] = 0.0;
}
const RideMetricFactory &factory = RideMetricFactory::instance();
QSet<QString> todo;
// hack djconnel: do the metrics TWICE, to catch dependencies
// on displayed variables. Presently if a variable depends on zones,
// for example, and zones change, the value may be considered still
// value even though it will change. This is presently happening
// where bikescore depends on relative intensity.
// note metrics are only calculated if zones are defined
for (int metriciteration = 0; metriciteration < 2; metriciteration ++) {
for (int i = 0; i < factory.metricCount(); ++i) {
todo.insert(factory.metricName(i));
while (!todo.empty()) {
QMutableSetIterator<QString> i(todo);
later:
while (i.hasNext()) {
const QString &name = i.next();
const QVector<QString> &deps = factory.dependencies(name);
for (int j = 0; j < deps.size(); ++j)
if (!metrics.contains(deps[j]))
goto later;
RideMetric *metric = factory.newMetric(name);
metric->compute(ride, *zones, zone_range, metrics);
metrics.insert(name, metric);
i.remove();
}
}
}
}
double secs_watts = 0.0;
QString intervals = "";
int interval_count = 0;
int last_interval = INT_MAX;
double int_watts_sum = 0.0;
double int_hr_sum = 0.0;
double int_cad_sum = 0.0;
double int_kph_sum = 0.0;
double int_secs_hr = 0.0;
double int_max_power = 0.0;
double time_start, time_end, km_start, km_end, int_dur;
QListIterator<RideFilePoint*> i(ride->dataPoints());
while (i.hasNext()) {
RideFilePoint *point = i.next();
double secs_delta = ride->recIntSecs();
if (point->interval != last_interval) {
if (last_interval != INT_MAX) {
summarize(intervals, last_interval,
km_start, km_end, int_watts_sum,
int_hr_sum, int_cad_sum, int_kph_sum,
int_secs_hr, int_max_power, int_dur);
}
interval_count++;
last_interval = point->interval;
time_start = point->secs;
km_start = point->km;
int_secs_hr = secs_delta;
int_dur = 0.0;
}
if ((point->kph > 0.0) || (point->cad > 0.0)) {
int_dur += secs_delta;
}
if (point->watts >= 0.0) {
secs_watts += secs_delta;
int_watts_sum += point->watts * secs_delta;
if (point->watts > int_max_power)
int_max_power = point->watts;
if (num_zones > 0) {
int zone = (*zones)->whichZone(zone_range, point->watts);
if (zone >= 0)
time_in_zone[zone] += secs_delta;
}
}
if (point->hr > 0) {
int_hr_sum += point->hr * secs_delta;
int_secs_hr += secs_delta;
}
if (point->cad > 0)
int_cad_sum += point->cad * secs_delta;
if (point->kph >= 0)
int_kph_sum += point->kph * secs_delta;
km_end = point->km;
time_end = point->secs + secs_delta;
}
summarize(intervals, last_interval,
km_start, km_end, int_watts_sum,
int_hr_sum, int_cad_sum, int_kph_sum,
int_secs_hr, int_max_power, int_dur);
summary += "<p>";
bool metricUnits = (unit.toString() == "Metric");
QDomDocument doc;
{
QString err;
int errLine, errCol;
if (!doc.setContent(QString(metricsXml), &err, &errLine, &errCol)){
fprintf(stderr, "error: %s, line %d, col %d\n",
err.toAscii().constData(), errLine, errCol);
assert(false);
}
}
QString noteString = "";
QString stars;
QDomNodeList groups = doc.elementsByTagName("metric_group");
for (int groupNum = 0; groupNum < groups.size(); ++groupNum) {
QDomElement group = groups.at(groupNum).toElement();
assert(!group.isNull());
QString groupName = group.attribute("name");
QString groupNote = group.attribute("note");
assert(groupName.length() > 0);
if (groupNum % 2 == 0)
summary += "<table border=0 cellspacing=10><tr>";
summary += "<td align=\"center\" width=\"45%\"><table>"
"<tr><td align=\"center\" colspan=2><h2>%1</h2></td></tr>";
if (groupNote.length() > 0) {
stars += "*";
summary = summary.arg(groupName + stars);
noteString += "<br>" + stars + " " + groupNote;
}
else {
summary = summary.arg(groupName);
}
QDomNodeList metricsList = group.childNodes();
for (int i = 0; i < metricsList.size(); ++i) {
QDomElement metric = metricsList.at(i).toElement();
QString name = metric.attribute("name");
QString displayName = metric.attribute("display_name");
int precision = metric.attribute("precision", "0").toInt();
assert(name.length() > 0);
assert(displayName.length() > 0);
const RideMetric *m = metrics.value(name);
assert(m);
if (m->units(metricUnits) == "seconds") {
QString s("<tr><td>%1:</td><td "
"align=\"right\">%2</td></tr>");
s = s.arg(displayName);
s = s.arg(time_to_string(m->value(metricUnits)));
summary += s;
}
else {
QString s = "<tr><td>" + displayName;
if (m->units(metricUnits) != "")
s += " (" + m->units(metricUnits) + ")";
s += ":</td><td align=\"right\">%1</td></tr>";
if (precision == 0)
s = s.arg((unsigned) round(m->value(metricUnits)));
else
s = s.arg(m->value(metricUnits), 0, 'f', precision);
summary += s;
}
}
summary += "</table></td>";
if ((groupNum % 2 == 1) || (groupNum == groups.size() - 1))
summary += "</tr></table>";
}
if (num_zones > 0) {
summary += "<h2>Power Zones</h2>";
summary += (*zones)->summarize(zone_range, time_in_zone, num_zones);
}
// TODO: Ergomo uses non-consecutive interval numbers.
// Seems to use 0 when not in an interval
// and an integer < 30 when in an interval.
// We'll need to create a counter for the intervals
// rather than relying on the final data point's interval number.
if (interval_count > 1) {
summary += "<p><h2>Intervals</h2>\n<p>\n";
summary += "<table align=\"center\" width=\"90%\" ";
summary += "cellspacing=0 border=0><tr>";
summary += "<td align=\"center\">Interval</td>";
summary += "<td align=\"center\"></td>";
summary += "<td align=\"center\">Distance</td>";
summary += "<td align=\"center\">Work</td>";
summary += "<td align=\"center\">Max Power</td>";
summary += "<td align=\"center\">Avg Power</td>";
summary += "<td align=\"center\">Avg HR</td>";
summary += "<td align=\"center\">Avg Cadence</td>";
summary += "<td align=\"center\">Avg Speed</td>";
summary += "</tr><tr>";
summary += "<td align=\"center\">Number</td>";
summary += "<td align=\"center\">Duration</td>";
if(unit.toString() == "Metric")
summary += "<td align=\"center\">(km)</td>";
else
summary += "<td align=\"center\">(miles)</td>";
summary += "<td align=\"center\">(kJ)</td>";
summary += "<td align=\"center\">(watts)</td>";
summary += "<td align=\"center\">(watts)</td>";
summary += "<td align=\"center\">(bpm)</td>";
summary += "<td align=\"center\">(rpm)</td>";
if(unit.toString() == "Metric")
summary += "<td align=\"center\">(km/h)</td>";
else
summary += "<td align=\"center\">(mph)</td>";
summary += "</tr>";
summary += intervals;
summary += "</table>";
}
if (!errors.empty()) {
summary += "<p><h2>Errors reading file:</h2><ul>";
QStringListIterator i(errors);
while(i.hasNext())
summary += " <li>" + i.next();
summary += "</ul>";
}
if (noteString.length() > 0) {
// The extra </center><center> works around a bug in QT 4.3.1,
// which will otherwise put the noteString above the <hr>.
summary += "<br><hr width=\"80%\"></center><center>" + noteString;
}
summary += "</center>";
}
return summary;
}

64
src/RideItem.h Normal file
View File

@@ -0,0 +1,64 @@
/*
* 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_RideItem_h
#define _GC_RideItem_h 1
#include <QtGui>
class RideFile;
class Zones;
class RideMetric;
class RideItem : public QTreeWidgetItem {
protected:
double *time_in_zone;
public:
QString path;
QString fileName;
QDateTime dateTime;
QString summary;
QDateTime summaryGenerationTime;
RideFile *ride;
Zones **zones;
QString notesFileName;
typedef QHash<QString,RideMetric*> MetricMap;
typedef QHashIterator<QString,RideMetric*> MetricIter;
MetricMap metrics;
RideItem(int type, QString path,
QString fileName, const QDateTime &dateTime,
Zones **zones, QString notesFileName);
~RideItem();
QString htmlSummary();
int zoneRange();
int numZones();
double timeInZone(int zone);
};
#endif // _GC_RideItem_h

23
src/RideMetric.cpp Normal file
View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2008 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 "RideMetric.h"
RideMetricFactory *RideMetricFactory::_instance;
QVector<QString> RideMetricFactory::noDeps;

138
src/RideMetric.h Normal file
View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2008 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_RideMetric_h
#define _GC_RideMetric_h 1
#include <QHash>
#include <QString>
#include <QVector>
#include <assert.h>
#include "RideFile.h"
class Zones;
struct RideMetric {
virtual ~RideMetric() {}
virtual QString name() const = 0;
virtual QString units(bool metric) const = 0;
virtual double value(bool metric) const = 0;
virtual void compute(const RideFile *ride,
const Zones *zones,
int zoneRange,
const QHash<QString,RideMetric*> &deps) = 0;
virtual bool canAggregate() const { return false; }
virtual void aggregateWith(RideMetric *other) {
(void) other;
assert(false);
}
virtual RideMetric *clone() const = 0;
};
struct PointwiseRideMetric : public RideMetric {
void compute(const RideFile *ride, const Zones *zones, int zoneRange,
const QHash<QString,RideMetric*> &) {
QListIterator<RideFilePoint*> i(ride->dataPoints());
while (i.hasNext()) {
const RideFilePoint *point = i.next();
perPoint(point, ride->recIntSecs(), ride, zones, zoneRange);
}
}
virtual void perPoint(const RideFilePoint *point, double secsDelta,
const RideFile *ride, const Zones *zones,
int zoneRange) = 0;
};
class AvgRideMetric : public PointwiseRideMetric {
protected:
int count;
double total;
public:
AvgRideMetric() : count(0), total(0.0) {}
double value(bool) const {
if (count == 0) return 0.0;
return total / count;
}
void aggregateWith(RideMetric *other) {
assert(name() == other->name());
AvgRideMetric *as = dynamic_cast<AvgRideMetric*>(other);
count += as->count;
total += as->total;
}
};
class RideMetricFactory {
static RideMetricFactory *_instance;
static QVector<QString> noDeps;
QVector<QString> metricNames;
QHash<QString,RideMetric*> metrics;
QHash<QString,QVector<QString>*> dependencyMap;
RideMetricFactory() {}
RideMetricFactory(const RideMetricFactory &other);
RideMetricFactory &operator=(const RideMetricFactory &other);
public:
static RideMetricFactory &instance() {
if (!_instance)
_instance = new RideMetricFactory();
return *_instance;
}
int metricCount() const { return metricNames.size(); }
const QString &metricName(int i) const { return metricNames[i]; }
RideMetric *newMetric(const QString &name) const {
assert(metrics.contains(name));
return metrics.value(name)->clone();
}
bool addMetric(const RideMetric &metric,
const QVector<QString> *deps = NULL) {
assert(!metrics.contains(metric.name()));
metrics.insert(metric.name(), metric.clone());
metricNames.append(metric.name());
if (deps) {
QVector<QString> *copy = new QVector<QString>;
for (int i = 0; i < deps->size(); ++i) {
assert(metrics.contains((*deps)[i]));
copy->append((*deps)[i]);
}
dependencyMap.insert(metric.name(), copy);
}
return true;
}
const QVector<QString> &dependencies(const QString &name) const {
assert(metrics.contains(name));
QVector<QString> *result = dependencyMap.value(name);
return result ? *result : noDeps;
}
};
#endif // _GC_RideMetric_h

Some files were not shown because too many files have changed in this diff Show More