Compare commits

..

321 Commits

Author SHA1 Message Date
gcoco
d6ba7b0cb3 Merge pull request #8 from gcoco/master
Fixes for Manual Ride Dialog
2012-02-01 07:12:18 -08:00
Gareth Coco
3ad5401b0b Comment added and replace tabs with spaces 2012-02-01 10:10:12 -05:00
Gareth Coco
d9e54c2be0 Fixes for Manual Ride Entry
Includes:
- Resize dialog for Mac users to display all fields
- Modify display of data in fields
- Add Average HR check
- Base code formatting to remove tabs

Fixes #622
2012-01-31 23:18:07 -05:00
Gareth Coco
eeadc688bf Ordered build - qwt then src 2012-01-31 15:18:38 -05:00
Damien
ed9baf6fa3 Update french translation for rho estimator
modified:   src/translations/gc_fr.qm
	modified:   src/translations/gc_fr.ts

Fixes #616
2012-01-31 13:47:51 -05:00
Alejandro Martinez
3115bec92f Rho Estimator Translation 2012-01-27 15:32:48 -05:00
Gareth Coco
2294b85e3b Update translation files 2012-01-26 13:19:16 -05:00
Gareth Coco
d1a1d56d6f Allow CdA to lowest possible in AerolabWindow.cpp
Fixes #475
2012-01-26 12:59:12 -05:00
gcoco
cd576a4e27 Merge pull request #7 from rodericj/patch-1
There was what looked like a copy paste error. I changed the zone 7 labe...
2012-01-26 07:12:18 -08:00
Steven Gribble
03a221c558 This patch adds an air density (rho) calculator feature to GoldenCheetah.
Users supply the estimator with measurements of temperature,
air pressure, and dew point, and the estimator uses Herman Wobus'
model for vapor pressure, plus the standard equations for
calculating air density, to estimate Rho. This feature is
useful for dialing in the Rho value in aerolab.

The patch hooks the estimator into the Tools menu as another dialog
box, similar to the CP estimator that's also under the tools menu.
2012-01-26 09:27:56 -05:00
Mark Liversedge
9ed9f7a6d0 Fix crash if TRIMP zones > 5
Fixes #483.
2012-01-24 16:23:59 -05:00
Jamie Kimberley
a78bd5d919 update French Translation
William Juban noted that we are incorrectly translating "Peak" as "Pique".
I have changes all occurences to the correct tranlation "Pic".

Fixes #613
2012-01-23 09:11:37 -05:00
Roderic Campbell
e4ece50af2 There was what looked like a copy paste error. I changed the zone 7 label to the appropriate 7 instead of 1 2012-01-22 16:49:37 -08:00
Jamie Kimberley
7611a44a54 Add other TRIMP metrics to performance manager.
Add the ability to use the TRIMP 100 and TRIMP ZONAL metrics in the
performace manager.

Fixes #535.
2012-01-22 15:42:28 +00:00
Gareth Coco
8e43eb31aa Fix interpolation of lat/lon when missing or 0/0
Fixes #597.
2012-01-22 14:55:00 +00:00
Mark Liversedge
a3f643e11c Support TrainerRoad.com TCX Files/Tcx speed in meters/sec The TCX parser ignored samples where distance is zero even when speed and time are available.
This broke reading files from TrainerRoad.com.
Thanks to Walter Bürki for pointing out speed is m/s and not km/h

Fixes #580 & #598.
2012-01-20 00:58:01 -05:00
Gareth Coco
d348344e0e Add a TCX ride exporter 2012-01-19 23:35:29 -05:00
LukeNRG
7b0fa7680d Updated German Translations 2012-01-19 23:35:19 -05:00
Mark Liversedge
565ba3f219 Fix zero speed in TxtRideFile for Imperial units
When importing files with imperial units the importer
does not set distance data and the speed is reset to zero.

This patch fixes that.
Please enter the commit message for your changes. Lines starting
2012-01-17 15:58:31 -05:00
Damien
db2b68bd9d Remove Joule warnings
Remove warnings for new format identifiers in the last Joule firmware.

Fixes #524
2012-01-17 15:41:36 -05:00
Mark Liversedge
d746dde5a3 Disable roch text in metadata
It causes significant performance issues when pasting complex font information,
we only allow plain text since we only serialise the plain text elements anyway.

Fixes #599.
2012-01-17 15:41:30 -05:00
Damien
ac43bccd6f Correct bug #496 for longitude < -65
Fixes #496.
2012-01-17 15:40:40 -05:00
Damien
ac2e202af4 Correct encoding in json parser
Fixes #408.

	modified:   src/JsonRideFile.y
2012-01-14 20:11:23 -05:00
Mark Liversedge
b107c4a1e6 FitRideFile distance of zero bug
Fit ride file contains samples where the distance
is set to zero, even if the previous sample is
non-zero.

The parser has been adjusted to keep the previous
distance used when a zero value is encountered.

Fixes #544.
2012-01-12 13:17:10 -05:00
Thomas Irps
dcb441b7e5 Portuguese translation 2011-12-22 10:24:41 -05:00
Gareth Coco
dae942a2f7 Add a Portuguese translation 2011-12-22 10:24:35 -05:00
Mark Liversedge
97cedad044 Add Virtual Power for 1UP USA bike trainer
Fixes #560.
2011-12-22 09:38:15 -05:00
Mark Liversedge
8fec614a5e Forward support in .json for temp/slope
But it will be unused in v2.1 since there is nowhere
to store the data. But at least the json parser won't
break.
2011-12-03 23:33:59 +00:00
David
5463be1a2f Updated Czech translations 2011-11-28 13:57:14 -05:00
Gareth Coco
e2e063015e Updated Spanish Translation
Co-authored by:

  Alejandro Martinez <amtriathlon@gmail.com>
  Arturo <argmac@gmail.com>
2011-11-28 09:56:12 -05:00
LukeNRG
7b7ff9622e Updated German Translation 2011-11-28 09:55:32 -05:00
Mark Liversedge
cd6b86c3eb Better Bounds Checking in RideFile::intervalBegin()
Return value when out of bounds had a fencepost error. Annoyingly
it is exactly the same fencepost error that was fixed in a line
of code 10 lines lower in the source.

This fixes rare issues with rides where intervals start at the
end of the ride file. This can happen with rides that have been
split.
2011-11-28 09:29:07 -05:00
Gareth Coco
b01c007ed2 Initialise LAT/LON to zero (0.0) in the parser
If there is no LAT/LON data often GC will set really small numbers
to LAT/LON which the map functions will try to map.

Fixes #522
2011-11-25 20:57:39 -05:00
Mark Liversedge
acdde3e02a Merge branch 'master' of github.com:/srhea/GoldenCheetah 2011-11-25 16:56:28 +00:00
Mark Liversedge
6b07997791 Fix FTDI Adaptor on Linux and Mac
Eric Brandt provided a fix for the new FTDI adaptor
sold with Computrainers from late 2009. It only fixed
the problem under Windows.

This fix applies the same modification to turn off
hardware flow-control for Linux and Mac.

Fixes #523.
2011-11-25 16:44:23 +00:00
Mark Liversedge
440a0b8404 Use strtod() to convert text to double
To get improved precision when parsing pasted
data in the ride editor.

Fixes #477.
2011-11-22 10:04:11 -05:00
Gareth Coco
d3f16313bb Fixes for RealtimeController
Adds in missing breaks.
Set power to zero when no cadence.

All this from v3 code base.
2011-10-27 23:14:03 -04:00
Mark Liversedge
fa0aa3fd75 Add virtual power for LeMond Revolution
The LeMond revolution trainer has been analysed in
some depth by Tom Anhalt and as a happy by-product
idenitfied the power/speed curve.

Since Darren Hague had already added the ability to
approximate power from Speed when training on a turbo
this patch extends that to include the LeMond device.

All the analysis Tom performed can be read over on
wattagetraining.com here:
http://wattagetraining.com/forum/viewtopic.php?t=335

Fixes #498.
2011-10-27 23:13:44 -04:00
Mark Liversedge
0d811ba4ba Updated translation files.
To include latest translations and also the update
from Keisuke Yamaguchi.

Fixes 489.
2011-10-26 16:50:55 +01:00
Mark Liversedge
9dff6e0cf6 Summary fixups
* Show time in zone as a percent
* Annotate heading to show units

Fixes #473.
Fixes #474.
2011-10-11 20:48:46 -04:00
Mark Liversedge
b04e308d9f Check Date/Time unique
Issue a warning if the user changes the ride date/time
to the same as an existing ride.

They can still go ahead, but when saving it will overwrite
the existing file.

Fixing the save routines to check would require significant
refactoring and can be fixed at a later date.

Fixes #466.

[code refactored from commit dfaf151 release3 and folded in manually]
2011-10-06 20:09:02 +01:00
Mark Liversedge
63fe2fb443 Merge branch 'master' of github.com:/srhea/GoldenCheetah 2011-10-06 19:02:55 +01:00
Mark Liversedge
34001d30b5 Fix Split Ride file loss bug
In some instances split ride will refuse to overwrite existing
ride files (where they have the same date/time). This patch
increments the start time by one (or more) seconds to ensure
there is no conflict.

Fixes #165.
2011-10-06 19:00:02 +01:00
Keisuke Yamaguchi
769fd633e2 Update Japanese translation files
Fixes #459
2011-09-28 22:27:53 -04:00
Gareth Coco
eb456cee63 Update gc_fr.qm for updated French translation 2011-09-14 21:57:11 -04:00
lemaitre
afe4710bf5 Updating of the French translation
Updating of the French translation file ts
2011-09-14 21:56:35 -04:00
Gareth Coco
b977abffcc Update translation .qm files
Needed for the translation changes from #451
2011-09-13 16:23:20 -04:00
Alejandro Martinez
8e1ec66820 Enable 3d Plot Translation and include spanish translation
Fixes #451
2011-09-13 16:22:29 -04:00
Gareth Coco
adc8430e0e Changed KPH to km/h in displays
To match International Standards
2011-09-13 12:51:10 -04:00
Gareth Coco
65109d95fd Changed kph to km/h in displays
International standards show metric speed as km/h
2011-09-13 11:57:35 -04:00
Mark Liversedge
189f11d80f Revert "SplitRide overwrites existing file"
This reverts commit 183564d1ea.

Small risk of losing data, reverting to previous behaviour.
2011-09-07 07:01:00 +01:00
Mark Liversedge
183564d1ea SplitRide overwrites existing file
Fixes #165.
2011-09-02 18:20:52 +01:00
Jamie Kimberley
076190161f clean up wishlist web page 2011-09-01 11:45:57 -04:00
Jamie Kimberley
f43a71f3a4 Update user guide steps 1 and 2
Updated the user guide to reflect the fact taht the FTDI drivers are
optional for most systems.  Changed the instructions for linux install to
reflect that we are now distributing gzipped archives.  I also added a in
page link to the wiki.

Fixes #259
2011-09-01 11:44:08 -04:00
Mark Liversedge
c383ee75fb Merge branch 'master' of github.com:/srhea/GoldenCheetah 2011-08-30 22:26:04 +01:00
Mark Liversedge
e0711b4bb7 Fix Download Ride Dialog instructions refresh
When changing the device type in the Download
Ride dilaog the instructions do not change to
reflect the device selected. This patch fixes
that.

Fixes #434.
2011-08-30 22:23:22 +01:00
Gareth Coco
5ce1617667 Comment out QwtDesigner build in qwtconfig.pri.in 2011-08-30 10:32:01 -04:00
Alejandro Martinez
0091defb6c Update Spanish Translation
- Summary Metrics
 - Move up/Move down/Include/Exclude Buttons in Metric Selection
 - Hr Peak Power xx min
 - Macro download instructions
 - Elevation Offset in Aerolab
2011-08-30 10:21:50 -04:00
Gareth Coco
3d93c0ca52 Reorder liboauth includes
Depending on how libcurl is built, it must appear before libcrypto
2011-08-29 12:31:28 -04:00
Jamie Kimberley
8fcbe81daf force use of no elide and scrollbars in main tab
depending on the style used on a given system the text in the tab bar for
selection of different charts (summary, ride plot...) may be elided on
screens with small resoulution.   This overrides the use of scrollbars
which is set  explicitly in the code. this one line patch forces the use of
non-elided text in the the tabs so that scroll bars appear.
2011-08-28 23:14:02 +01:00
Mark Liversedge
524ab81a08 Fix FitRideFile for header change
The header size in FitRideFile has increased from
12 to 14 bytes, but the new field is at the end of
the header, not in the middle.

Additionally, there is a new global record (79) which
we now silently ignore.
2011-08-27 23:52:56 +01:00
Mark Liversedge
ad9a76a172 Support FR310xt latest firmware
Fit file format can now have a 12 and 14 byte header. This
patch adds support for 14 byte headers since this is required
by the latest 310xt firmware.

Fixes #430.
2011-08-27 09:07:45 +01:00
Damien
5b838d99c2 Correct bug in the peakPowerHr formula 2011-08-23 23:24:18 -04:00
Damien
b7bfb98dc8 Aerolab : Change LCD display to LineEdit
Also fixes v3 crash when changing CdA.

Fixes #423.
2011-08-23 22:18:24 +01:00
Mark Liversedge
7352245a91 Fix Macro device data crash
If you attempt to download from a Macro device
and use wrong device or initial read fails then
it crashes, this patch fixes that.

It does not fix more general data errors but should
at least mean incorrect user selections do not result
in GC crashing.

Fixes #366.
2011-08-21 21:16:41 +01:00
Mark Liversedge
bfa68faeda Update .gitignore for Lex/yacc
Ignore temporary files generated when lex
and yacc generate parsers from a grammar.
2011-08-21 16:51:18 +01:00
Mark Liversedge
95d6a40ecd Fix date/time handling when importing rides
The ride import wizard would only allow the user to
change the ride date/time if it was a .gc .json or
.csv file. This was (wrongly) because we could not
update the date/time defined within the ride file itself
(we cannot write in other formats e.g. wko).

Of course, we encode the ride date/time in the filename
and so it could be changed. However, not all the RideFile
readers supported this.

To get around this, the import wizard now does let you
change the date and time for any file type and the ride
file factory method openRideFile() will override whatever
date and time is returned by examining the filename. The
user needs to double click the date or time to edit it.

Additionally, the select date... combo would only register
when you changed the selection, it now defaults back to
the 'select date..' option after each selection.

Lastly, the 'choose date' function now works as advertised
and triggers editing the date for the ride selected.

[commit cd86521 cherry picked into master from release_3.0.0dev]

Fixes #11
2011-08-21 15:43:06 +01:00
Mark Liversedge
3a4b782a16 Merge branch 'master' of github.com:/srhea/GoldenCheetah 2011-08-21 15:17:45 +01:00
Mark Liversedge
7331775e40 Add JSON support to v2.1
Patch to allow v2.1 to read v3.1 .json ridefiles.

Version 3 introduces a new GC file format
using Javascript object notation (json). In
version 3 files are written in this format
as a local, native format thus deprecating the
XML .gc notation.

This backport to 2.1 does not;
* write in json format, it just reads files
* set RideFile::rideId, since it is not present in v2

To build you will need lex/yacc (or flex/bison) the
instructions are within gcconfig.pri.in but are the
same as for version 3.

Fixes #395.
2011-08-21 15:12:01 +01:00
Damien
0c7abe9755 Modify Aerolab to add interval highlight and zoom + auto offset
Fixes #241
Fixes #147
2011-08-17 13:50:08 -04:00
Mark Liversedge
9ab5fb26e2 Guess ride date time for Poweragent CSV files
File name is in the format "name yyyy-mm-dd hh-mm-ss.csv".

[cherry picked into master from c08dae in release_3.0.0dev]

Fixes #281.
2011-08-07 22:53:30 +01:00
Mark Liversedge
a641ec7e0a Fit files sometimes go backwards
FIT record type '253' occasionally causes time to go
backwards, this might be a decoding error, but for now
we force time to go forward anyway.

Looking at bad files GPS data with this patch suggests
this is the correct behaviour.

Fixes #104.
2011-08-06 17:58:12 +01:00
Damien
c513a47e07 Modify TRIMP formula to use time_riding instead of workout time
Fixes #355.
2011-08-06 12:08:44 +01:00
Mark Liversedge
a562d2f73b Fix LogY Intervals on Histogram
The LogY function for histograms was overlooked
when implementing intervals. The baseline and
start/end values of the interval curve needed to
be set to non-zero values to match the main curve.

Fixes #396.
2011-08-06 11:48:18 +01:00
Damien
dfbb1c29f8 Correct interval 2011-08-05 22:40:05 +01:00
Damien
b0cca3c2fa Modification to handle odd start page and compatible with qt 4.6 2011-08-05 17:01:16 +01:00
Damien
8c2eac427b Add a setFocus on the Treelist to correct a MacOs Bug of Qt
Fixes #255.
2011-08-05 15:14:38 +01:00
Mark Liversedge
880c97c639 Don't allow Nan or Inf sample values
I thought this was introduced previously and was not. When
appendPoint adds a new sample it now sets non-finite values
to zero.
2011-08-05 15:03:34 +01:00
Mark Liversedge
614f267a5e Merge branch 'master' of github.com:/srhea/GoldenCheetah 2011-08-05 14:54:37 +01:00
Mark Liversedge
14f7924c28 Fix SummaryWindow crash
When a large number of intervals are defined (>50 or so)
then the RideSummary window crashes.

It appears to be a bug in QTextEdit. It does a double free
when setHtml is called, after a 'large' text item was
set. It may be a QString bug.

To avoid the issue we use a QWebView instead of a QTextEdit
to display the summary and then try and set fonts to match
the application.

[cherry-picked and merged from release_3.0.0dev]
2011-08-05 14:53:58 +01:00
Gareth Coco
7a501a9699 Make interval period on map user defineable
Previously the map track was broken up into 30 second intervals.
This patch allows the user to define a time interval they want.

Fixes #273
2011-08-04 23:30:36 -04:00
Mark Liversedge
f1238fcce6 Fix Save when old .bak exists
If you save a ride and then delete it. The re-import and save
you will end up with two copies of the ride in the ride list.

This is because when we save the first time the original file
is renamed to e.g. ride.tcx.bak and the new ride.json is then
created. All is well.

But then delete the ride and it will rename ride.json to
ride.json.bak. Again, All is well.

Now, re-import the ride. We now have; ride.tcx.bak and
ride.json.bak and ride.tcx. Again, all is well.

But now, if you make changes and save it will attempt to
rename ride.tcx to ride.tcx.bak AND FAIL. This is because
the old ride.tcx.bak file is there. It will then create
ride.json. All is NOT well, since we have two rides with
the same date and time but different extensions.

This patch fixes this by unlinking ride.ext.bak before
trying to rename the file.

[folded in manually from commit #1d135a in branch release_3.0.0dev]
2011-08-03 20:19:19 +01:00
Mark Liversedge
4784408106 Fix annoying gap in CP curve. 2011-08-03 14:25:24 -04:00
Damien
182208c145 Bug #178 TRIMP is not updated after modification in Rest Hr Tag -> compute metric after save
Fixes #178
2011-08-03 13:59:51 -04:00
Mark Liversedge
00959bed8c Better support for Negative, Inf, NaN and High Values
Some ride file formats use -1 to indicate sensor not
present or data loss (e.g. TPX) and on occasion a NaN
or Infinite value will be presented.

This patch handles this by converting negative data sample
values to zero and handling out of bounds values when
selecting zone ranges.

This is not a substitute for better handling of poor ride
data but it reduces the effect.

Also fixes #311.
2011-08-03 13:40:20 -04:00
Mark Liversedge
57c7260a19 Fix .man crash in CP plot
Old .man files contain insufficient data to
compute a CP curve and caused an array bound
crash.

Fixes #205.
2011-08-02 02:14:57 +01:00
Mark Liversedge
a70cf8ebc0 Fix RideEditor find dialog for 'between'
The find dialog expected the between values
to be small and high, this patch will find
values between regardless of whether the
search values are lo/hi or hi/lo.

Fixes #351.
2011-08-01 23:52:36 +01:00
Frank Zschockelt
0f9b82a750 Included 4 sample sigma files in the test/rides directory 2011-08-01 23:41:30 +01:00
Frank Zschockelt
8cca3c088a Support for Sigma SLF/SMF file formats
A ridefile reader for Sigma .slf/.smf format files.

Fixes #90.
2011-08-01 23:39:54 +01:00
Mark Liversedge
d84ffec0a6 Fix RideSummaryWindow crash
The recent patch to allow users to configure which
metrics to display on the ride summary window is not
forwards compatible. If metrics referenced no longer
exist (i.e. they are from a future release or have
been deprecated) GC will crash.
2011-08-01 23:36:49 +01:00
Damien
20477e1670 Altitude scale present with no altitude data.
Uncheck the channel if not present. Fixes #295.
2011-08-01 22:17:20 +01:00
Mark Liversedge
610b2ea2d0 Remove console error for seasons.xml
There is no need to warn about seasons.xml missing, it
is quite acceptable to have none set. Worse still sending
to the console log is next to useless for users that
don't launch from the command line.

Partial cherry pick from 4972f2472e (v3.0.0dev)
2011-08-01 13:22:23 -04:00
Mark Liversedge
e7a7803f09 Fix WKO+ file reader GPS 'drops'
The WKO+ file format appears to record drops in
recording of GPS data with a latlon of 180,180. We
expect this to be 0,0.

This makes the WKO+ file reader consistent with the
GoogleMapControl and removes the need to clean data
there.

If it is found that 180,180 is the standard way of
recording drops in GPS signal then we can change the
code. We use 0,0 since it is conveniently at sea off
the west coast of Africa.

Cherry pick d06c9e97c9 (From v3.0.0dev)
2011-08-01 13:14:31 -04:00
Damien
c6a376b89f Add PeakPowerHr metric (average HR during peak power) 2011-07-28 21:12:20 +01:00
Damien
65c4d89890 Correct crash with truncated files
Fixes #337
Fixes #354
2011-07-28 12:41:55 -04:00
Rainer Clasen
1674558dbb FitRideFile: turned assertions into graceful fail
reading Fit files with Smart recording and a certain pattern of timestamps
could cause assertions. This shouldn't happen, as it's no Programming error.

Changed the checks into graceful failures.

Unfortunatly I don't have any files to test this.

fixes #364
2011-07-25 14:07:14 -04:00
Rainer Clasen
ee79a86c1f SrmRideFile: turned assertions into graceful fail
reading unsupported SRM files caused assertions. This shouldn't happen, as
it's no Programming error.

Changed the checks into graceful failures.

fixes #364
2011-07-25 14:07:07 -04:00
Mark Liversedge
92897a966b Better rounding of time in AllPlot
With realtime data there will often be samples with
timestamps like 940.002 and 940.998. This cuases an
issue on the ride plot, where it believes there is
no sample for 941 and therefore plots a zero value.

This patch rounds the timestamps to the nearest 100th
of a second, which is consistent with the mechanism
used in the ride editor.
2011-07-24 21:36:21 -04:00
Damien Grauser
ca1c4def3a O_Sync Macro X device support
Adds support for the Macro X bike computer for downloading rides
and reading/writing in the native file format (sync).

For more information on this new bike computer see:
http://www.o-synce.com/en/products/bike/macro-series/macrohigh-x.html

Fixes #357.
2011-07-24 10:44:46 +01:00
Damien
05346eda24 Add summary metrics list to preferences
Fixes #317.
2011-07-23 17:41:37 +01:00
Rainer Clasen
bea79092ab Fit: handle unknown fields gracefully
So far the FIT parser bailed out, whenever it found something
unknown/uninterested to GC. This is quite orthogonal to the FIT design, as
it's supposed to be extended.

renamed read_<foo> functions to match the FIT base_type names.

unified handling of "unavailable/invalid" values - i.e. if sensor data is
temporary unavailable. This allows easier and consistent handling -
especially for the uintXz base_types, which only differ by a different
"invalid" value. Had to change the type of the "values" list to int64 to
fit uint32/int32, as well.

added proper support for signed integer types. I'm wondering, why lon, lat
+ temperature were decoded properly...

added support for currently unsupported base types by just skipping their
bytes. This allows us to continue reading.
2011-07-23 13:57:49 +01:00
Rainer Clasen
e478c24650 Fit: support big/little endian data
... on both, big and little endian machines.

Fit reader only supported little endian data on little endian machines.

All values read from FIT files are now swapped (if neccessary) according
to file and system endianess.

fixes #287
2011-07-23 13:57:49 +01:00
Rainer Clasen
96bb21d04d Fit: ignore unknown message types
do not bail out on unknown message types. This violates the design of the
Fit format of being extensible.

As this was the last thing using the global_msg_names QMap, I've nuked
this, aswell.
2011-07-23 13:57:49 +01:00
Alejandro Martinez
65615295c2 Add HrZones to Weekly Summary and Fix Spanish Translation
Fixes 344.
2011-07-23 12:09:18 +01:00
Rainer Clasen
b21d24039c fix reading signed values from srm files
seems, the assumption for "speed" in SRM7 files being unsigned was wrong.
Powercontrol/SRMWIN seem to use negative speed as "invalid".

Furthermore altitude may become negative, as well.

To address this, QDataStream now does the bit-swapping and speed +
altitude are read as signed values.

Fixes 346.
2011-07-23 12:08:56 +01:00
Rainer Clasen
c25f920062 whitespace cleanup
unfortunatly my latest patches introduced some tabs. Replaced them with
spaces to meet GC indent style.

Fixes 347.
2011-07-23 12:08:56 +01:00
Alejandro Martinez
7f2b6dd793 Set UTF-8 in charts,metadata and seasons xml files
Fixes 345
2011-07-23 12:08:56 +01:00
Damien
148390ea61 handle showHr/Speed/Cad/Alt state for stacked view
Fixes 130.
2011-07-23 12:08:56 +01:00
Gareth Coco
c525a36ea5 Change start date from UTC to localtime for bin ride file
Fixes 338.
2011-07-23 12:08:56 +01:00
Damien Grauser
627595175f Update French translation. 2011-06-07 22:31:47 +01:00
Alejandro Martinez
09365a8b24 Add Spanish Translation & Fix Translation Issues
This patch:
* Add Spanish Translation
* Enable default Zone Descriptions Translation
* Enable Color Selection Translation
* Enable PM Metrics and Metric Detail Dialog Translation
* Enable Data Processor Translation
* Enable Metrics Translation
* Enable Metadata Translation
* Enable Several Incomplete Translations
* Add default lang selection based on locale
* Fix Trend-Watts translation Add units default
* Translate Download Instructions
* Add default charts translation

Notes:
1. If ENABLE_METRICS_TRANSLATION is defined, the code setting metric names
   and units is moved from constructors to initialize method, to be called
   after translator initialization, English Name is preserved as InternalName
   for metadata.xlm compatibility in metric override.

2. Q_DECLARE_TR_FUNCTIONS(class-name) macro is used to set tr() context
   when class-name is not QObject sub-class.
2011-06-05 17:46:46 +01:00
Mark Liversedge
8ae7a3f738 Fix WKO parser for Ergomo users
The WKO ride file reader used wrong constants for the bit
field size of the sample data when decoding files from
Ergomo devices.

Fixes #335.
2011-05-25 20:04:19 +01:00
Mark Liversedge
2ef0533ec3 Revert "Add Spanish Translation and Enable Metrics Translation"
This reverts commit 1fbaeae611.

Accidentally pushed to the main repository along with recent
patch fixups. Ale is working on a complete solution for
supporting translations in metrics, metadata and configuration
settings. This patch was an initial version and should not
have been pushed to master.
2011-05-23 21:37:08 +01:00
Mark Liversedge
b7632a4173 Fix Virtual Power for Fluid2
Fix was applied to v3 but not v2, an oversight on my part.
The patch is now applied to the current master for v2 users.

Fixes #270.
2011-05-23 21:09:51 +01:00
Mark Liversedge
29fa978b8f Fix Wko GPS parsing on 64bit
This bug recently fixed in v3 is also present in the
v2 code.

Fixes #214.
2011-05-23 20:24:57 +01:00
Alejandro Martinez
1fbaeae611 Add Spanish Translation and Enable Metrics Translation
Add Spanish Translation.
If ENABLE_METRICS_TRANSLATION is defined, the code setting metric names
and units is moved from constructors to initialize method to be called
after translator initialization and QObject::tr(s) calls are replaced by
QApplication::translate("class name",s) to set appropiate context.
Metric symbol is used for symbol override instead of metric name.
2011-05-17 19:28:56 +01:00
Tim Shaffer
d91337e18d Make the default date range for Performance Manager a user preference. 2011-03-02 21:07:50 +00:00
Gareth Coco
88967c1588 Set default Smoothing (secs) in Ride Plot to one (1) second
Fixes #145
2011-03-01 21:30:12 +00:00
Mark Liversedge
dd7af03785 Updated Czech translation
Thanks to David Kramar for an updated version of the
Czech translation files. This provides 100% of texts
translated.
2011-03-01 21:08:31 +00:00
Damien
0c1353aa39 Remove error logs for unused datas in ride file
Fixes #173
2011-02-27 11:07:49 +00:00
Ilja Booij
8cb592d6b4 fix 'Wrong distance on Tacx caf file import'
This fixes wrong distance on Tacx by taking distance of first data
point as the base, and basing all distances on that first point.

Fixes #204
2011-02-27 11:07:49 +00:00
kohasa
09bec66b38 enabled editing interval duration by keyboard.
Fixes #125
2011-02-27 11:07:48 +00:00
Gareth Coco
c4f379d12c FIT file reader fixes
1. Allow FIT reader to recognise the file comes from a Garmin Edge 800
This is Garmin product ID 1169 in the decodeFileId routine.

2. Ignore global_msg_type = 72
This message appeared with the introduction of the Garmin Edge 800.
There is no FIT SDK that tells us what this message is.
It appears only once and has timestamp/device serial number.
Code now recognises the msg_type as valid but we don't process it.

3. Add all decodeEvent types and work only with "timer" events
Not all event_types were present. They are now all in the function.
Previously the decodeEvent would look at all "events"
We now only decode event_types if the event is of type "timer".

Fixes: #250
2011-02-26 14:09:30 +00:00
Gareth Coco
1101e7e62b Changes to map markers
1. Removed green begin marker
2. Change interval markers 2 and higher to be blue
2011-02-26 14:09:30 +00:00
Greg Lonnon
7aceb4f0f3 Added a QFilesystemWatcher to monitor adding files to the workout directory.
This allows the TrainTool to automatically pick up any changes added to
the directory
2011-02-26 14:09:30 +00:00
Rainer Clasen
57b9e28110 Added SRM5 file format read support
SRM5 basically is the same as SRM6, but lacks "blocks". This means, it
only has the date of the exercise and no further absolute time info.
Furthermore it can't flag periods of time, where no data was collected.

Due to lack of absolute time, Exercises start at 0:00, by default.

Fixes #208
2011-02-26 14:08:54 +00:00
unknown
0bf19e3e8d This patch correct altitude for TCX files converted from FIT files These files doesn't have altitude for each Trackpoints. I propose to not assign 0 to altitude before each trackpoint
Fixes #60
2011-02-26 14:07:53 +00:00
Eric Brandt
6815fe0d1f fix realtime mode load timer and lcd sig. digit display issues
The load timer was simply being accumulated with each firing
of the timer. This resulted in inaccuracies. The fix is to
accumulate using a timer that measures the duration between loadUpdate
calls.

The speed, average speed, gradient, and distance LCDs ought to always
display 1 significant digit to avoid bouncing. This is now fixed.

This commit fixes #262 and fixes #263.
2011-02-14 22:05:42 +00:00
Mark Liversedge
8913b37346 Fix 'Save data' in RealtimeWindow
A previous commit stopped disk updating from working, i.e. saving
workout data to a .csv file. This patch fixes that.

Fix supplied by Greg Lonnon, Fixes #254.
2011-02-07 19:48:31 +00:00
Mark Liversedge
3ff839c4ff Fix Computrainer with Stereo FTDI adaptor
The newer Racermate FTDI based USB adaptor (USB-StereoJack) failed
to receive data from the Computrainer, this was due to incorrect
flow control settings.

Fix supplied by Eric Brandt.
2011-02-07 19:25:25 +00:00
Damien
470885df50 Modify csv import for ergomo file with comma or semicolon separator
Fixes #244.
2011-01-30 15:50:24 +00:00
Darren Hague
152239eea4 Remove toMSecsSinceEpoch() and work around
toMSecsSinceEpoch() is from Qt 4.7. Replaced with an implementation
based on QTime:start() and QTime.elapsed() from Qt 4.6.
There is now a theoretical upper limit on turbo sessions of 24 hrs :-)

Fixes #247.
2011-01-30 15:14:13 +00:00
Darren Hague
073079a6e7 Add virtual power support for BT-ATS trainer
Add "BT Advanced Training System" to dropdown.
Implement 3rd-order polynomial to get power from speed.

Fixes #246.
2011-01-30 14:43:11 +00:00
Darren Hague
963c28c7a8 Use realtime clock for realtime-mode clock
Instead of adding 200ms to the elapsed time on every gui update,
this patch records the timestamp of when the Start/Pause buttons
are clicked and subtracts these from the current hardware clock time
to calculate elapsed total & lap times.  Fixes bug #235.
2011-01-22 20:24:52 +00:00
Darren Hague
7e42cdd486 Corrected - to + in CycleOps formula
Fixes #239
2011-01-22 20:24:52 +00:00
Mark Liversedge
c1fc674609 Translations Bonanza!
Russian Translation from Gwelu
Czech Translation from Beeda
Updated Japanese Translation courtesy of Key
Updated German Translation courtesy of Luke

Golden Cheetah now supports 7 languages!
2011-01-22 20:19:42 +00:00
Darren Hague
615737658d Virtual Power; Better GSC-10 pairing support.
Virtual Power - included patch from Mark Liversedge & corrected bug
with his help.

GSC-10: Check dual sensor 4th and speed-only sensor (which is rare) as
5th.
This means that an all-Garmin setup (ANT+ stick, GSC-10 sensor) will
always work. A speed-only sensor is almost useless in GC real-time
mode anyway, because speed-only sensors tend to run off the front
wheel which will be stationary on a trainer. Fix pointer problems with
device controller/config.

Fixes #219
2011-01-09 19:15:20 +00:00
Justin Knotzke
3cab2f6175 Update of German translation by LukeNRG 2011-01-06 17:36:20 -05:00
Greg Lonnon
93ad436c6a changes to the markers in google maps.
blue marker = start of ride
red marker = end of ride
green markers = interval markers.

interval markers has the interval metrics displayed.
end marker has the ride summary displayed.

Fixes #169.
2011-01-02 17:21:17 +00:00
Mark Liversedge
550ae22aa3 Merge branch 'master' of github.com:/srhea/GoldenCheetah 2010-12-25 23:08:40 +00:00
LukeNRG
ea11916a93 German Translation
Provides 100% coverage of language texts*

* a couple of texts for WeeklySummary window containing html
  codes (gt,lt et al) had not retained their encoding using
  &gt, &lt, whilst I edited a few of them in the file, these
  two were particularly complex and challenging. We can fix
  them later. [Mark Liversedge]
2010-12-25 23:07:25 +00:00
Justin Knotzke
ff3a052415 Cycleops Test Ride 2010-12-21 18:13:49 -05:00
Roberto Massa
57a2c27262 Italian Translation
Provides translation for 47% of language texts.
2010-12-19 14:48:41 +00:00
Bruno Assis
c780f2edd0 Portugese (Brazil) Translation
Provides 64% coverage of language texts.
2010-12-19 14:38:38 +00:00
Mitsukuni Sato
4b0ce34d09 Japanese Translation
Provides 100% coverage of translatable texts.
2010-12-19 14:21:47 +00:00
Greg Lonnon
241976634d GPX RideFile Support
The current format will parse time, and gps data.  It will interpolate
distance and speed based on time and gps.
2010-12-17 21:05:29 +00:00
Jamie Kimberley
c21ca878c0 Update download page to reflect build of mac 10.4 2010-12-12 21:07:41 +00:00
Mark Liversedge
30ea14bcd5 Mac PPC binary added to downloads page. 2010-12-12 18:14:14 +00:00
Mark Liversedge
e1f9bbf62e Add link to wiki from the website. 2010-12-03 08:38:12 +00:00
Mark Liversedge
71b512c1a9 Joule support fixed on download page. 2010-11-30 21:18:24 +00:00
Mark Liversedge
544718099a Updated www.goldencheetah.org for v2.0 release. 2010-11-30 20:52:39 +00:00
Mark Liversedge
789be5681d New screenshots for the website. 2010-11-29 21:08:43 +00:00
Mark Liversedge
66d32bbebf tweak LTM bar chart gaps. 2010-11-29 10:42:16 +00:00
Mark Liversedge
7f55855f2d Plot bars side-by-side on Metric charts
This patch plots bars next to each other, rather than overlaying
them making the data much easier to interpret. The code is inspired
from Robert Carlsen's weekly summary.

The "transparency" for the bar colors has been adjusted to make
the coloring more vobrant since we no longer need to 'see through'
bars to see what is underneath.
2010-11-29 10:08:06 +00:00
Mark Liversedge
532fe0d26c Disable Ride Editor for manual or null rides. 2010-11-27 22:00:23 +00:00
Mark Liversedge
a29109343f Fix CSV parser and more checks for NULL ride
The CSV ride parser now checks for empty rides and returns
a NULL ride if there are no samples. In addition, the rideEditor
tries to set editorData even if the ride is NULL. Lastly, the
RideItem code for lazy reads of RideFile data didn't check for
NULL values (!).
2010-11-27 21:52:02 +00:00
Mark Liversedge
5f13f4800b Fix Stop(DEVICE_OK) connect error in RealtimeWindow
The signal/slot connection in RealtimeWindow for the 'stop'
button fails. The code wants to pass DEVICE_OK to the slot
but the QWidget connect method wants a SLOT signature.

This patch sets a default of DEVICE_OK for the newly
introduced 'status' parameter to the Stop() method and
corrects the signature used in the connect statement. As
a result, if the Stop() button is pressed the status will
be 0 (DEVICE_OK).

If you press start then stop really quickly and there *is*
a device error then it will still create a CSV file with no
samples. The CsvRideFileReader should be fixed to parse
these files correctly.
2010-11-27 21:32:58 +00:00
Justin Knotzke
12fb154f5b Fixed bug whereby CSV file was corrupted.
When Device was not available, Realtime was creating a corrupted CSV.
If Device is not availble at startup, the ride file is removed.
2010-11-25 10:22:59 -05:00
Mark Liversedge
72604b6cb3 Prettify Bar Charts in LTM
The bar charts on the metrics tab are a bit on the
quirky side. This is an artefact of the qwt library's
lack of a bar chart style.

This patch adopts the same approach used in weekly
summary charts and the histogram 'by zone' plots.

Fixes #185.
2010-11-09 19:53:02 +00:00
Mark Liversedge
dde17f278b TRIMPPoints use workout_time not time_riding
TRIMP was using time_riding to calculate points
even though the HR average is for the entire ride.
In addition, time_riding may be zero when only
HR data is present (there is no way of determining
time spent or not spent riding).

Fixes #187.
2010-11-09 19:43:12 +00:00
Mark Liversedge
7e25eedb8e Workaround for hidden files on Win32
Ride files may be created as hidden files in some
instances on Windows 32. This patch ensures that
they are subsequently included in the ride list
(i.e. ride files can be hidden files).

Fixes #176
2010-11-06 08:28:45 +00:00
Mark Liversedge
d483445291 Too many TRIMP metrics on Summary
This patch removes the additional TRIMP metrics
on the ride summary, plain old TRIMP points are
shown but not the zonal and 100 versions.
2010-11-06 08:25:39 +00:00
Damien
33076c1cb5 Correct TRIMP HR equation.
Fixes #177
2010-11-06 08:23:54 +00:00
Damien
d6d9bd9227 Fix HR add zone crash
Fixes cannot add new HR zone (v2.0-RC1)

Fixes #172
2010-11-03 06:01:38 +00:00
Rainer Clasen
59ae5fc537 Fix SRM interval start/end
While the last patch for misaligned SRM Intervals did fix the out of
bounds indices (and thereby fixed the crash), it got the start/end
swapping wrong. In other words: It swapped start/end where it shouldn't
and therefore broke all interval handling.
2010-11-02 06:19:18 +00:00
Gareth Coco
0e5e5206a2 Font issues in 3D plot on Linux
Updated gcconfig.pri.in to advise Linux users who have font issues
with the 3D plot tab, to download the latest 0_2_x code for qwtplot3d.

Fixes: #88
2010-11-01 17:23:27 +00:00
Ilja Booij
f6b5cd2790 Fix Polar ride parser crash
Tacx HRM exporter does not create an [IntTimes] section.
When the [HRDate] section is encountered, This patch creates
a single interval in the intervals list, which has the
length of the complete ride.

Fixes #23
2010-11-01 14:12:56 +00:00
Mark Liversedge
87a6f9e628 Typo in HistgramWindow for checking HR/Power ranges
The code in HistogramWindow::rideSelected() to check
HR and Power zones have been configured for the
ride just selected contained a typo; the power range
would be set to HR and the hr range would be set to
power. This resulted in the wrong options being shown
in the drop down box.

This patch fixes this behaviour.
2010-11-01 13:26:44 +00:00
Mark Liversedge
9d9b447044 HR Zone Shading on Histogram
Last piece of the TRIMP/HR enhancemens to the
hisogram plot; enables HR zone shading, HR zone
colors in options and also fixes the axis to
start at the lowest HR value present rather than
zero.
2010-11-01 11:26:10 +00:00
Mark Liversedge
da8e636e65 Fix PM day offset by 1
Almost certainly caused by a fencepost errors somewhere else
in the code. This fixes the offset by subtracting one from the
offset used.

Fixes #28
2010-11-01 06:47:40 -04:00
Mark Liversedge
e3c6e7e76c Fix config pane ugly scrollbars on Linux. (twitter icon) 2010-10-31 23:52:15 +00:00
Damien
819303b060 Zonal TRIMP 0 if no HR. 2010-10-31 23:31:55 +00:00
Mark Liversedge
fc583e6404 Fix WKO+ files with Alt, Wind or Slope
The logic in the WKO+ ridefile parser tried to
re-use code blocks for working with alt, wind
and slope but as a result ended up overwriting
variables and losing data as a result.

It also mishandled negative values for those
data series.

It also falsely reported iBike files as Ergomo.

Fixes #164
2010-10-31 21:45:50 +00:00
Damien GRAUSER
830e4efd3d HR Zones and TRIMP Metrics
This patch introduces new functionality for working with
Heartrate based data.

* HR Zones can be defined, from Resting, Maximum and Lactate HR
* TRIMP metrics are calculated; TRIMP, TRIMP100 and Zonal TRIMP
* TRIMP metrics can be used to drive the PMC
* Time In Zone metrics for HR have been added
* Histogram window will now work with Power/HR zones
* User Settings have been added to record gender, weight and others
* RideFile has a new tag "Athlete" which is set to the athlete name

Fixes #140
2010-10-31 18:08:48 +00:00
Justin Knotzke
3e0f3358f5 Removed superfluous call to creating a CSV file. 2010-10-31 07:02:52 -04:00
John Ehrlinger
f690e188a1 Update the Polar hrm file import to include a conversion for files recored in english units.
S-series records in english
CS-series records in metric.
2010-10-26 19:25:18 -04:00
John Ehrlinger
cffde51caa Add a collection of polar hrm files for testing purposes.
These files include english and metric units. Files are imported from s610/s725x/cs400 units. Include different data types.
2010-10-26 19:23:35 -04:00
Ilja Booij
5e2f52cf28 Some .caf files have data blocks which have an extra 8 bytes per record. I do not know what the contents of these 8 extra bytes are, but to make things work, we need to at least take them into account when parsing the files.
The caf parser now checks what version the file is (100 or 110) and
handles data records accordingly. Files with version 100 have 10
bytes per data record, all of which are known. For version 110, the
first 10 bytes are the same as version 100, followed by 8 bytes per
data record.
2010-10-26 19:17:01 -04:00
Gareth Coco
603c56f595 Resolve Lat/Long issue on FIT file import
The FIT parser will attempt to interpolate data when filling in for smart
recording or if a record is missed. A problem occurs if one of the lat/long
points is missing or 0,0.

This patch will record a 0,0 lat,long if the record is missing in the FIT
file and when interpolating, will set any interpolated data points to 0,0
if the start or end record is also 0,0.

A 0,0 record is not plotted on the MAP tab.

Fixes #111
2010-10-26 19:12:37 -04:00
Gareth Coco
cee09061ff Correctly plot lat/long for CSV files
When writing out data point, lat/long were transposed.

Fixes: 136
2010-10-26 19:06:20 -04:00
Jamie Kimberley
91a66a7520 Fix gaps in ride now keeps previous altitude
The patch changes the behavior of the "fix gaps in rides" function. This
now fills stops in rides with the last recorded altitude rather than zeros
as was previous behavior.

fixes #158
2010-10-26 19:02:53 -04:00
Mark Liversedge
d106086afe Fix WKO+ with Powercontrol VI RideFile reader crash
Files kindly supplied by Alex Simmons have demonstrated a bug in the
parsing of WKO+ files that contain data downloaded from an SRM
Powercontrol VI. The files are parsed incorrectly and often lead
to crashes or absurdly high summary values.

This might be version specific, since the files were WKO v3 files, we
should watch for WKO+ v2.2 files that contain Powercontrol VI data and
potentially make this version specific if needed.
2010-10-25 21:20:50 +01:00
Jim Ley
0238ba9052 More help/feedback during entry of manual workout
Improve fix of #132 so that it doesn't require you to enter
a bikescore or daniels point or distance if you do not want to.

Add hints to the appropriate values for the entries.

Fixes #132
Fixes #146
2010-10-25 17:45:08 +01:00
Mark Liversedge
acc522748a Make ride file pattern case insensitive. 2010-10-22 21:46:19 +01:00
Rainer Clasen
99c330edc2 tolerate swapped marker in srm files
in certain circumstances srmwin seems to (have?) written bad files: The
first/last data chunks referenced by a marker were swapped plus the "real"
start index was 0 - although chunks are counted from 1.

This patch checks for this defect and interprets the data in the same way
as recent srmwin versions do when reading files with this defect.

Though the way marker are stored in PCV makes me guess, the refernce the
bad chunk index 0 really means "last" chunk in the recording.
2010-10-14 10:12:53 -04:00
Austin Roach
ea05ba2151 Tweet Ride Error Checking
Adds error checking to Tweet Ride.  Checks to make sure that oauth
credentials exist, that tweet is under 140 characters, and that
oauth_http_post received a reply indicating a successful post.

Move addition of hashtag to getTwitterMessage() to cleanup length
calculations.
2010-10-11 10:27:35 -04:00
unknown
0d6949ed9e Changed validation in manual ride entry.
Fixed warning on Bike score estimate label not being used.

Added validators to Bikescore and daniels points manual entry to allow
more than 3 digits.

Added dialog which enforced validator on distance, bikescore and
daniels points.

This fixes #132
2010-10-01 21:33:10 +01:00
Mark Liversedge
2f41cabb1d Re-order crypto/oauth libs for linking
liboauth depends upon libcrypto but they are declared in
the opposite order in src.pro. This patch reverses this to
avoid linker errors on Linux.
2010-10-01 21:30:14 +01:00
Ilja Booij
07e086ce44 Support Tacx CAF Ride File Format
Initial version of Tacx Caf file importer. TacxCafRideFile.cpp added to qmake file.
Fixed parsing of heart rate value, Heart rate and cadence should have used quint8
instead of qint8, because they're unsigned.

Fixed #143.
2010-10-01 13:49:38 +01:00
Mark Liversedge
f31cef3f1e Racermate/Ergvideo TXT file support
This patch add support for the Racemate text export of their
CDF file format. This format has also been adopted by Ergvideo v3.

Fixes #144.
2010-10-01 13:49:33 +01:00
Rainer Clasen
a4828070df be more precise on SRM support on the start page
To save users fruitless work trying GC on non-supported SRM Powercontrols.

Fixes #107.
2010-09-28 22:24:40 +01:00
Mark Liversedge
781e0619ec Fix SEGV in connect error for QT4.6
Fixes #80.
2010-09-28 22:21:04 +01:00
Jamie Kimberley
a8e5777953 Modify line endings in the ride editor
On mac systems, when copying data from excel and pasting into the ride
editor GC would complain about the data ranges not being the same size.
This is a result of the fact that excel mac terminates lines with a CR
(\r) rather than a NL (\n).  This patch changes the behavior so that we
check for all three commonly occurring line endings CR, NL and CRNL.

fixes #135
2010-09-28 22:13:14 +01:00
Ken Sallot
dd5cdd920d Improved handling of smart recording / gaps in TCX files
Currently, GC interpolates time gaps in all TCX files as a result of smart
recording.  However, this overlooks periods of inactivity (stopped at a light
for instance).

1. Provide a configuration option, that if enabled, will tell GC to interpolate
time gaps in TCX files as if they were a result of smart recording.  If the
option is not enabled, then interpolation will not occur.

2. Provide a maximum "high water mark" (in seconds) for these time gaps.  The
default HWM is 25 seconds, but is user configurable.

Any time gaps that exceed the high water mark can be fixed via the Fix Gaps
tool under the toolbar.

Fixes #74.
2010-09-28 21:56:27 +01:00
Justin Knotzke
0139b7ee84 Missing #ifdef if not building with Twitter support. 2010-09-05 14:37:07 -07:00
Justin Knotzke
78111d4279 Missing #ifdef if not building with Twitter support. Thanks John Ehrlinger 2010-09-05 14:24:35 -07:00
Justin Knotzke
899e6eb362 Support for OAuth for the Twitter feature. Metric adjectives by Robert Carlsen. 2010-08-26 14:06:25 -04:00
Mark Liversedge
f6eb97ec0f Add support for Google Earth (KML)
This patch adds an 'Export to KML' option to the ride
menu. It will create a .kml file including power, hr,
torque etc. These can be viewed alongside the map view
in Google Earth 5.2.

Please note this requires libkml. The features of libkml
that are required were introduced in revision 852 which
means that as of Aug 2010 you will need to checkout from
the SVN source repo and build;

svn checkout http://libkml.googlecode.com/svn/trunk/ libkml-read-only

and the ./configure mantra that worked successfully for
me on Mac OS X was;

./configure CC="gcc -arch i386" CXX="g++ -arch i386" --disable-swig

Building on WIN32 is currently fraught with issues, unless
you build via MSVC 2010. Linux is straight forward but you will
need to install / apt-get libcurl.

Fixes #133.
2010-08-26 10:47:49 +01:00
Mark Liversedge
48a25081ed Fix blank space on right of AllPlot. 2010-08-14 13:00:05 +01:00
Damien Grauser
5e575beaff Support for Joule BIN File Format
The new source files, missed from previous commit.
2010-08-10 18:55:55 +01:00
Damien Grauser
861fa5ee38 Support for Joule BIN File Format 2010-08-10 18:54:07 +01:00
Mark Liversedge
fef237f138 PWX Ride file support
Support for Training peaks new .pwx file format. This
is an XML format (and is particularly verbose). Support
has been added to enable interoperability with WKO+ v3,
TrainingPeaks.com and Device Agent.
2010-08-06 19:48:51 +01:00
Justin Knotzke
21572977b4 When DFPM watts were detected, GC was still using fake iBike watts. 2010-08-05 16:08:51 -07:00
Mark Liversedge
9109eb616f Fix crash in AllPlot on Manual Ride
When a manual ride is selected whilst on allplot
it will refresh the plots, but computes invalid
offsets into the ride data. this patch fixes this
crash.

Fixes #128.
2010-08-04 22:19:46 +01:00
Mark Liversedge
de0b28f9dc Fix gcc 4.5 compile errors
Gcc 4.5 considers explicit calls to object constructors to
be errors. This patch corrects these to enable building using
this current release of the GNU C++ compiler. This is required
to support building on Windows with the current Qt 4.6 sdk
since it includes gcc 4.5.

It is worth noting that this relese of the GNU compiler also
spots two or three uninitialised variables too, I will correct
these at a later date.
2010-08-04 21:35:09 +01:00
Mark Liversedge
02fe34216d Fix lots of SEGV in AllPlot for new cyclist (rideItem is null). 2010-07-30 06:43:54 +01:00
Robert Carlsen
081856bf64 Updating stack zoom control enabling logic. 2010-07-29 21:16:22 -04:00
Robert Carlsen
62d658c929 Disable stack zoom controls when stacked view is disabled. 2010-07-29 02:16:22 -04:00
Mark Liversedge
29069cd63f Fix crash on openRideFile
The recent data processor / ride editor patch added some
post-processing actions after a ride file is opened. The
functions are called without checking for an open ride
failure. This patch fixes that stupid error.
2010-07-29 06:48:06 +01:00
Sean Rhea
0fe01407a2 link to Gareth's devel builds from main site 2010-07-28 19:48:05 -07:00
Mark Liversedge
efd4de62a1 Fix Imperial Show By Distance on AllPlot
The recent patch for allPlot zooming does not display the full plot
properly when the user has selected imperial units and is displaying
by distance (not time). This minor fix corrects this.
2010-07-28 20:23:01 +01:00
Mark Liversedge
2962fce0b7 Scrolling AllPlot and Tooltip
Introduce a span-slider on the normal Ride Plot chart to enable
users to select a range to plot and scroll left and right. The
zoom to interval function now uses this slider rather than setting
a zoom range. Tooltips are now displayed on the normal and stacked
views to assist in reviewing specific data points.

Old style zooming still works as expected, but scrolling at a zoom
level is not yet implemented.

The qxt widget 'QxtSpanSlider' has been placed in the top
level directory (GoldenCheetah/qxt) alongside the qwt widgets to
avoid adding another dependency.

A number of optimizations have also been introduced to speed up
plotting in general. A new color setting for the thumbnail plot
has also been introduced. Refresh of the plot when data is changed
in the editor has been fixed. The zoom scale up/down widgets are
also disabled when in normal mode to avoid the bug highlighted by
Robert Carlsen.

Fixes #122.
2010-07-27 19:46:35 +01:00
Justin Knotzke
8e73d01829 Fixed a small typo in QHttp. 2010-07-26 06:51:29 -07:00
Justin Knotzke
ca24400a0a Merge branch 'origin' 2010-07-25 07:54:29 -04:00
Justin Knotzke
4465d18b4f Twitter image for the Config Dialog page. 2010-07-25 06:59:53 -04:00
Justin Knotzke
3e2468dcc6 Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-07-25 06:57:23 -04:00
Justin Knotzke
e1d69f71e1 Tweet your ride. You can now tweet your ride from GC 2010-07-18 15:15:26 -04:00
Mark Liversedge
1a71a8a41f Fix RideEditor crash on right-click Column
RideEditor::colMapper was not initialised in the
constructor leading to an erroneous delete when
first referenced and a subsequent crash.
2010-07-18 14:12:54 +01:00
Justin Knotzke
1506c41cca Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-07-17 14:45:45 -04:00
Mark Liversedge
cd3bbc4e64 Ride editor and tools
A new tab 'Editor' for manually editing ride file data points and
associated menu options under 'Tools' for fixing spikes, gaps, GPS
errors and adjusting torque values. A revert to saved ride option
is also included to 'undo' all changes.

The ride editor supports undo/redo as well as cut and paste and
"paste special" (to append points or swap columns/overwrite
selected data series). The editor also supports search and will
automatically highlight anomalous data.

When a file is saved, the changes are recorded in a new metadata
special field called "Change History" which can be added as a
Textbox in the metadata config.

The data processors can be run manually or automatically when a
ride is opened - these are configured on the ride data tab in
the config pane.

Significant changes have been introduced in the codebase, the most
significant of which are; a RideFileCommand class for modifying
ride data has been introduced (as a member of RideFile) and the
RideItem class is now a QObject as well as QTreeWidgetItem to
enable signalling. The Ride Editor uses a RideFileTableModel that
can be re-used in other parts of the code. LTMoutliers class has been
introduced in support of anomaly detection in the editor (which
highlights anomalies with a wiggly red line).

Fixes #103.
2010-07-17 14:33:39 +01:00
Justin Knotzke
fc668303da Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-07-06 17:35:20 -04:00
Mark Liversedge
8569f812a6 Fix assert crash in BestIntervalDialog.cpp
Removed redundant assert in BestIntervalDialog.cpp. It is redundant
because it is executed prior to a logic check for the same condition.

The assert check has been shown to be unreliable due to inherent
inaccuracies in float arithmetic and comparisons for example, according
to the IEEE specs 1000.2 - 1000.0 will be stored as 0.200012. This
inherent problem with floats is particularly relevant in this
code since it is dealing with interval durations and recording intervals
which are expressed to a precision of 100th or even 1000th of a second.

Fixes #99.
2010-07-06 14:35:13 +01:00
Justin Knotzke
0b7140161c Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-06-07 06:32:18 -04:00
Mark Liversedge
06e44b8d47 Fix drag slider from Maps tab
When viewing the maps tab it is possible to drag and drop
the slider causing a file import dialog to pop-up and fail.
This patch rejects any drop events where the url is http.

Fixes #97.
2010-06-06 18:43:22 -07:00
Mark Liversedge
51784f64f6 Increase precision of sample data in .GC file format
Three related issues fixes; firstly lat/lon values lose precision when
being read from .tcx files by Qt's QString::toDouble(). This
has been replaced with the stdc strtod() function in TcxParser.

Secondly, when writing to .gc format precision was also lost, this
has been fixed for lat/lon values.

Thirdly, when writing to .gc format precision of seconds was lost,
this is particularly relevant to Powertap files which have a sample
rate of 1.26 seconds.

Fixes #83.
2010-06-06 18:39:26 -07:00
Mark Liversedge
c4f82e19b6 Better Zones Configuration Page
The zone ranges configuration page caused a SEGV when deleting the
last zone. On inspection the zone configuration needed to be
revised since the UI was confusing and didn't allow fine grained
user editing (relying upon manual editing of the power.zones file).

The UI has been redesigned and fine grained editing of ranges, zones
and default zones is now supported.

The Zones class has been slightly modified to support the new UI and
existing members are better commented. In addition, the read/write
functions have been updated to always include the DEFAULTS section and
to set defaults according to manual zone setups when it is not present
(legacy support).

There are now 10 TimeInZone metrics to match the maximum of 10 zones
the user can define.

Fixes #78.

Fixes #34.
2010-06-06 18:36:50 -07:00
Gareth Coco
7941f0a9bc Add read power from variable ns3:Watts.
TCX files exported from Garmin Connect have power as follows:
<Extensions>
              <ns3:TPX>
                <ns3:Speed>2.236999988555908</ns3:Speed>
                <ns3:Watts>68</ns3:Watts>
              </ns3:TPX>
</Extensions>

Fixed parser to read this as a valid power reading.

Fixes #65
2010-06-06 18:26:59 -07:00
Gareth Coco
d03d538498 Mapping and Latitude/Longitude logic changes
Patch changes the valid latitude/longitdue selection alogrithm.
Ensures that the data points are valid (-90<=Lat<=90, -180<=Long<=180)
Tightens up .WKO file import issues.
Allows for missed GPS data points of 0/0 in Garmin FIT files.
Changes mapping function to not plot invalid lat/long values.

Fixes #75
2010-06-06 18:20:43 -07:00
Justin Knotzke
8741af20ff Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-05-01 16:01:48 -04:00
Mark Liversedge
a0c5669166 Support for Polar SRD file format
A ridefile reader for Polar .srd format files. The code is
largely based upon code from the "s710" project.

Since "s710" is dependant upon GD and a number of deployment tools
and the fact that the code hasn't changed since May 2007 the
workout code has been included directly into the SrdRideFile.h
and SrdRideFile.cpp source files.

2 sample SRD files have been included in the test/rides directory
which were kindly supplied by Ian Charles.
2010-05-01 10:34:19 -07:00
Mark Liversedge
3f4adc2d31 Use Zone Colors on Map
A patch to use the GC zone color scheme (or preferences) when
plotting power on the Map tab.

fixes #82
2010-05-01 10:26:58 -07:00
Mitsukuni Sato
fc7dce1634 japanese translation 2010-05-01 10:22:25 -07:00
Mark Liversedge
803383ba2c Fix StressCalculator SEGV
If no results are returned from metricDB the stress calculator will
SEGV. This will occur when the DBVersion is out of sync with the
metricfactory (and this should not occur).
2010-05-01 10:21:03 -07:00
Mark Liversedge
9ae79db4cb fix SEGV in AllPlot on manual/null rides
fixes #71
2010-05-01 10:19:46 -07:00
Mark Liversedge
74f85f08d5 Show LAT/LON on 3D Plot
Add latitude and longitude to values that can be plotted on the
3D plot model window.

fixes #43
2010-05-01 10:14:34 -07:00
Justin Knotzke
85ee3859e4 Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-04-11 17:42:24 -04:00
Mark Liversedge
a6917a682a Metrics & Maps update for Win32 installer
The new metrics feature requires the SQLITE plugins to be
deployed alongside the binary and the forthcoming Google
Maps patch requires QtWebKit, Phonon, Xml and Xmlpatterns
deployed. Additionally, the terrain view requires the
jpeg image formats deplyed.

This patch updates the win32 NSI script to include these
new dependencies and has been tested for Win 7, Vista and
XP SP3.

fixes #51
2010-04-11 17:37:46 -04:00
Julian Baumgartner
d77c6ed576 added the javascript routine to do mouse overs 2010-04-11 17:22:34 -04:00
Sean Rhea
ac570c415c make sure "current" is initialized
Also, a nit: fix indentation of 1 line in AllPlotWindow.h.
2010-04-11 17:19:33 -04:00
Patrick J. McNerthney
4ca86c441d Enhanced src/src.pro to detect the standard Debian/Ubuntu install set up of qwtplot3d and use that if QWT3D_INSTALL is not defined. 2010-04-11 17:10:46 -04:00
Sean Rhea
fbdd15f218 fix Daniels EqP when Time Riding == 0.0 2010-04-06 21:37:27 -04:00
Sean Rhea
afb9a44fe2 fix ugly formatting 2010-04-06 21:36:58 -04:00
Mark Liversedge
1751d8bf12 Remove update 'flicker' when tab selected
The recent patch to reduce redraws when rides are selected
causes each tab to redraw whenever the tab IS selected. This
patch reduces this by remembering the current ride plotted.

It also fixes the "double draw" in GoogleMapControl and AllPlot
when selected for the first time.
2010-04-04 17:28:07 -04:00
Justin Knotzke
fdb7d124d9 Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-04-02 09:53:47 -04:00
Sean Rhea
07623bf94f fix metrics for missing data
...by avoiding divide-by-zero errors.
2010-04-02 09:45:19 -04:00
Justin Knotzke
906b6f0a18 Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-04-02 06:39:55 -04:00
Mark Liversedge
744294dd53 fix crash for man .gc file import
checks for empty datapoints and also checks for overrides for time
and distance.
2010-04-01 10:29:14 -04:00
Mark Liversedge
38df7c28bd User Configurable Colors
A new config pane for defining color preferences for chart
curves, shading, background and grid lines et al. Default values
echo the current hard-coded values.
2010-04-01 10:29:13 -04:00
Mark Liversedge
778d651f00 User Configurable Metadata
User configurable data entry for recording information about
each workout.

FEATURES:
* Config UI for defining tabs and fields to maintain
* Config UI for defining keywords and colors
* Data maintenance UI on RideSummaryWindow
* "Special" Metadata fields are related to current variables
* Read/Write new fields/metric overrides via GcRideFile
* Metadata extraction in WKO files
* Calendar uses keyword and color config
* Numeric metadata is plottable on the Metric charts
*Metric refresh has been optimised
2010-04-01 10:29:13 -04:00
Justin Knotzke
f820627fda Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-03-25 16:37:59 -04:00
Mark Liversedge
b1f71eda4c Deprecate .man in favour of .gc
Save manual files in .gc format (using overrides) instead
of writing in csv format to a .man file. The .man file is
still supported via ManualRideFile but no longer created
by ManualRideDialog.
2010-03-25 09:16:29 -07:00
Mark Liversedge
d64fc6ea85 NULL/Empty ride checks
RideItem or RideFile or dataPoints() may be null or empty. This
is especially true of manual ridefiles. This patch adds some
checks for this situation and acts accordingly. Additionally, the
disable/enable of tabs depending upon ridefile type has been
adjusted to also include files with not dataPoints.
2010-03-25 09:16:29 -07:00
Mark Liversedge
4e7e6cfb3a Honour RideFile::startTime
When saving the value of startTime should be checked to see
if the filename/notes need to be renamed. In addition, RideItem
now allows the startTime to be modified and reflected in the
ride list. When importing .gc ridefiles the file is serialized
with the correct startTime if the user edited it during import.
2010-03-25 09:16:28 -07:00
Mark Liversedge
02a60735f0 Simplify RideMetric by using less pure virtual functions
Primarily to make override() a base class function that can be
used for any metric rather than expecting each metric to provide
a local version.

Also, add explicit notion of "average" vs "total" ride metrics, as
it will let us improve how the metrics DB handles averages later.
2010-03-25 09:16:22 -07:00
Justin Knotzke
5c2a2f0527 Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-03-24 06:03:49 -04:00
Sean Rhea
281c357605 quiet possibly meaningless warning 2010-03-23 21:06:45 -07:00
Sean Rhea
d1f003c190 the FIT saga continues
New idea: only linearly interpolate between two consecutive "record"
messages.  I don't know what else to do.  My FIT files have all sorts
of weirdness I can't explain.  One, for example, has two consecutive
start events with the same timestamp.  What does that mean?

This is all proof that just having the file "format" isn't really
enough.  What you need is the file *semantics*, and we don't have that
for FIT yet.
2010-03-23 20:59:58 -07:00
Sean Rhea
cd4fe5fe2e combine setActive and rideSelected
And pay attention to MainWindow::activeTab, such that only the active
tab redraws itself when changing rides.  This change really increases
GC's responsiveness when scrolling through the ride list.
2010-03-21 22:03:12 -07:00
Sean Rhea
92725db36a ignore records when time is stopped
I'm not really sure what these are doing in the FIT file Jamie sent
to the list, but there's only one of them, and it's at the end of a
long rest period, so it seems safe to ignore it for now.
2010-03-21 21:59:25 -07:00
Justin Knotzke
d2831baacc Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-03-21 12:57:34 -04:00
Sean Rhea
fbf4f988c9 assert.h is my nemesis 2010-03-21 08:57:45 -07:00
Mark Liversedge
636e8f3895 Add Headwind from WKO files
The attached exracts windspeed (+/-) from WKO files to
support the recent patch for headwind.

fixes #57
2010-03-21 08:41:11 -07:00
Mark Liversedge
f8ab4b03dc Honour metric precision in LTM
The metrics plot did not honour the metric precision setting
when displaying a hover tooltip. This resulted in BikeScore and
other values being rounded inappropriately and inconsistently.

This patch utilises the metric->precision() setting and displays
values consistently when compared to the ride summary. In addition,
the precision for LTS/STS on the LTM PM has been set to 1 decimal
place where previously it was zero.

fixes #50
2010-03-21 08:39:14 -07:00
Mark Liversedge
4552cafdb6 MRC File Format Fix
The Racermate MRC file format for workout files did not
honor TIME/PERCENT format files correcty. The code is a
little confusing because it mixes the device mode and
workout file format. Ideally, the file format and device
mode would be kept as separate state settings, but this
patch at least fixed the bug.

fixes #40
2010-03-21 08:35:22 -07:00
Mark Liversedge
883dbb448f Remove 'None' option from 3d
The none option for data selection on the 3d plot is a
misnomer. It suggests that dimensions can be removed. For
example plotting x/y only. In reality, the 3d libs and the
code for managing the plot go to great lengths to ensure a
plot is rendered across all dimensions.

The None option has been removed for this reason. A 2d
scatter plot with user selectable data series should be
coded specifically to render 2d scatter plots.

fixes #30
2010-03-21 08:34:17 -07:00
Sean Rhea
a217243456 fix FIT files with smart recording
Linearly interpolate missing points *except* between a pair of stop
and start event records.
2010-03-19 09:12:16 -07:00
Sean Rhea
affed979ba fix HR in FIT files with no HRM
The value 255 means, "no heart rate".
2010-03-18 08:03:56 -07:00
Mark Liversedge
32d67f21eb FitRideFile Linux fixups
Small mods to changed the capitalisation of the Qt includes and
add stdint.h uint16_t et al.
2010-03-17 22:48:41 -07:00
Sean Rhea
c0437f30e3 read Garmin FIT files
There is still the mystery of what global message type #22 is, but
other than that concern, this code seems to work pretty well now.
2010-03-17 08:22:31 -07:00
Justin Knotzke
26b00e184c Merge branch 'master' of git://github.com/srhea/GoldenCheetah 2010-03-15 17:26:51 -04:00
Sean Rhea
7f2abaa01b fix uninitialized 'ride' variable in ModelWindow 2010-03-14 19:26:07 -04:00
Mark Liversedge
ccc2f1f0ff Fix UTC to Localtime Error in GcRideFile
The GcRideFile stores the ride start datetime in UTC, on write the
datetime is correctly converted to UTC.

This patch fixes the GcRideFile reader to convert in the oposite
direction. Currently the code reads the UTC date as a local format
date - as a result the convert to localtime call does nothing.
2010-03-14 19:19:40 -04:00
Sean Rhea
e0f6cf23e6 fix SRM interval alignment
SrmRideFile was setting RideFilePoint::interval correctly for
each RideFilePoint, but it was setting the start and stop of each
RideFileInterval it created to 1 RideFilePoint later than they
should be.  This patch fixes things so that RideFilePoint::interval
and RideFileInterval::start|stop agree about the interval bounds.
2010-03-13 11:26:02 -05:00
Damien Grauser
f980142110 update French translation 2010-03-13 10:44:30 -05:00
unknown
0785cf863e Update french translation Committer: Damien Grauser modified: src/translations/gc_fr.qm modified: src/translations/gc_fr.ts 2010-03-12 17:54:14 -05:00
Mark Liversedge
19fe016567 make calendar font 2pts smaller than default 2010-03-12 11:30:47 -05:00
Damien Grauser
ff3a232863 remember user's preference for stacked view
...and add zoom.

fixes #58
2010-03-12 10:45:08 -05:00
Sean Rhea
bd28d3b28e maybe fix problem with Set CP button not enabled 2010-03-12 10:45:08 -05:00
Justin Knotzke
c6d28370ec #includes need to be in quotes to build under Windows. 2010-03-11 09:08:24 -07:00
Sean Rhea
f523fd5d3c allow the user to hide tabs
...and remember their tab hiding preferences across restarts.
2010-03-09 20:59:34 -05:00
Greg Lonnon
e47847c19e fixed the maps refresh issue 2010-03-09 13:14:03 -05:00
Greg Lonnon
6919d186bf metrics are now displayed correctly, alt isn't working right
fixed the avg speed and alt

added a possible change for the maps issue

Maps tab is always showing, it will either show the ride data, or No GPS Data Present
Page loading is async with the ride being selected.
Weird WKO gps data issue reported by Mark L is fixed (hopefully)  I see different results than Mark L
moved Sean's cp patch to the new location

Signed-off-by: Greg Lonnon <greg.lonnon@gmail.com>
2010-03-09 13:14:03 -05:00
Sean Rhea
0fd735e16d fix maps crash when no CP is set
The check in the maps code for whether a CP is set was backwards, causing
a segfault on the subsequent call to Zones::getCP().  To reproduce, create
a new cyclist and import a ride with lat/lng data.  The import succeeds, but
GC crashes when you click "Save" and on all subsequent restarts.
2010-03-07 08:24:20 -05:00
Andy Froncioni
a02bfaf810 Added headwind to Aerolab calculation for iAero
Added a headwind data field, which is available when using
an iAero head unit, to dramatically improve the calculation
of Chung analysis for users of more recent iAero devices.

All other data files than the iAero have the headwind term set to
zero when they append a point.
2010-03-06 13:11:36 -05:00
Damien GRAUSER
afdc862cc2 add ride plot stacked view 2010-03-06 11:47:55 -05:00
Sean Rhea
19223e51b3 add an xml pretty-printer
This makes it a lot easier to see what's going on in a TCX file.
2010-03-03 10:54:30 -05:00
Sean Rhea
37a17f11f8 remove eol space 2010-03-03 09:32:43 -05:00
Sean Rhea
19aff56946 always build with maps support 2010-03-03 09:32:43 -05:00
Sean Rhea
378b6f3537 fix up tabs 2010-03-03 09:32:43 -05:00
Greg Lonnon
268afe536c untabify the GoogleMapsControl files 2010-03-03 09:32:43 -05:00
Greg Lonnon
04b698f255 fixed the zone == -1 defect 2010-03-03 09:32:43 -05:00
Greg Lonnon
f97bc9c152 fixed the bounds box and the zoom to center correctly 2010-03-03 09:32:43 -05:00
Greg Lonnon
a392b33b51 added resizeEvent back in, fixed a few variable names to be more standard and added some of Mark L's suggestions. 2010-03-03 09:32:43 -05:00
Mark Liversedge
81a43b5314 added Mark's fixes 2010-03-03 09:32:43 -05:00
Greg Lonnon
c2f3476569 added googlemaps, took a couple of suggestions from Julian on map defaults. 2010-03-03 09:32:43 -05:00
Justin Knotzke
d4edc12dc6 New Logo and Icons by Dan Schmalz 2010-03-02 20:48:25 -05:00
Sean Rhea
b68e55beca better axis labels in PM plot 2010-03-02 09:45:21 -05:00
Mark Liversedge
bc85a3b5fe Change WKO+ version error to warning
The WKO+ file format version is changing version numbers
at a fairly accelerated pace, but the general structure of
the files are still readable by the WkoRideFile reader.

This patch issues a warning rather than an error on new
files. Recent v29, v30 and v31 of the WKO+ file format have
all been parsed successfully.

fixes #47
2010-02-28 08:13:01 -08:00
Mark Liversedge
fda33927d4 Typo in Save Dialog
The dialog message spelt change as chage. This patch fixes that typo.

fixes #45
2010-02-28 08:11:08 -08:00
Mark Liversedge
7bb9cf5462 Long Term Metrics
A user configurable chart for showing ride metrics and
other calculated values over time.

* Uses SQLITE database to store metrics
* Supports any metric available from the metric factory
* Adds new MaxHr, VI, Peak Power and Time In Zone metric
* Also includes LTS/STS/SB for PM charting
* Aggregates in days, weeks, months or years
* Reads and Updates seasons.xml
* Adds cycles and adhoc date ranges in seasons.xml
* Date ranges can be selected on the plot with shift-left click
* Allows users to customise preferences for color, symbols et al
* Allows user to customise metric names and unit names
* Supports smooth curves and topN highlighting
* Has a linear regress trend line function
* Allows users to save charts to charts.xml
* A default charts.xml is built-in
* A chart manager to import/export/rename/delete charts etc
* Provides a tooltip to provide basic datapoint information
* Performance Manager adjusted to use the MetricDB
* User configurable setting for SB calculation (today/tomorrow)
2010-02-25 08:01:43 -08:00
Greg Lonnon
cef5cca454 returned error for encrypted files
Also fix compiler warning.
2010-02-24 08:47:38 -08:00
Thomas Weichmann
77eccc7797 fix min window width problem caused by fixed width sliders in aerolab 2010-02-20 16:18:32 -08:00
Dag Gruneau
e3ac6c799c quarqd - cadence, wheel rotation, error checking
Fixed a number of issues with data from quarqd inf and nan values where
inserted as valid data points and thus destoying all plotting in the
realtime window and in later analysis.

The unit was used to distinguish between the entities, thus rpm was
erroneously used as a cadence, rpm is used as the unit for wheel
rotation and for cadence.  This made the cadence useless together with a
PowerTap hub which reports both cadence and wheel rotation.

No error checking was performed on the received data, bad data is
ignored now.
2010-02-12 06:07:30 -08:00
Andy Froncioni
906900fb19 iBike test rides for use with Aerolab 2010-02-12 05:36:44 -08:00
Sean Rhea
4145282415 remove unused settings ptr 2010-02-12 05:27:40 -08:00
Sean Rhea
3eb5243a28 remove eol spaces -- no functional change 2010-02-12 05:23:55 -08:00
Andy Froncioni
885629a2f5 Adds Aerolab tab
This patch adds Aerolab, a virtual elevation module.  A
new tab is added in which the user can perform virtual
elevation analysis.  This version is a manual Aerolab,
where user can use the following sliders to elevation-match
to a known elevation profile:
Crr  -coefficient of rolling resistance
CdA  -aero coefficient of drag * frontal area
Eta  -drivetrain efficiency (to be used when using a crank-
      or bottom-brack-based power meter)
Mass -total mass of bike + rider
Rho  -density of air
E_offset -an elevation offset to align elevations
2010-02-12 05:19:59 -08:00
Andy Froncioni
07a393fb64 Adds DFPM functionality to iAero
Uses the iAero native "guesstimate" power value until a
non-zero dfpm value is seen.  From then on, uses dfpm as "watts".
2010-02-08 06:49:01 -08:00
Sean Rhea
da28b43d40 remember chosen histogram bin width across restarts 2010-02-07 09:41:52 -08:00
Sean Rhea
4f9850bd9d shorten tab titles
Making room for the long-term metrics tab.
2010-02-07 09:26:44 -08:00
Sean Rhea
f930c6f272 remember chosen PM metric across restarts 2010-02-07 09:13:04 -08:00
Sean Rhea
fae1ea3f92 fix linux file names for 1.3.0 2010-02-07 09:13:03 -08:00
Sean Rhea
4b615a6cda update contrib and download pages for 1.3 2010-02-06 11:38:57 -08:00
Sean Rhea
b491867386 release notes for GC 1.3 2010-02-06 11:38:57 -08:00
Sean Rhea
f323780848 fix FTDI required for SRM download bug
GC supports two download port types: serial ports and D2XX.  Before, if
either of these failed to load, the download dialog wouldn't show either
port type.  With this patch, if both fail, GC displays a warning, but if
either one succeeds, GC will proceed with only that port type.  This
change should fix the problem that users were having to download and
install both the FTDI drivers and the PL2303 ones in order to download
from the SRM PCV.
2010-02-06 11:38:57 -08:00
Robert Carlsen
9147369c41 Added sanity checking to ignore missing metrics
There is a possibility that ride metrics may become unavailable yet
remain requested by QSettings (stored in
~/Library/Preferences/org.goldencheetah.GoldenCheetah.plist on OS X).

This patch ignores any metrics listed in the preferences yet are not
supported by the running version of Golden Cheetah.
2010-02-06 11:38:57 -08:00
Sean Rhea
5cd621f800 add Erase Ride(s) button to download dialog
This is a workaround for the SRM erase bug.  It gives the user a way to
try erasing the device's memory without re-downloading a ride.
2010-02-06 11:38:56 -08:00
Sean Rhea
1f548d0b84 regenerate stress cache after config change
fixes #32
2010-02-04 05:10:57 -08:00
Sean Rhea
16bc8c2686 don't check dependencies until newMetric is called
Before, we checked them during addMetric, and that left us vulnerable
link-order errors.  With this patch, we wait until someone actually asks
for an instance of a metric, and then we check all metrics' dependencies.
That way, since the Ride Summary always creates at least one metric, we'll
still check the dependencies of them all.  We just do it a little later in
the program's execution than before.
2010-02-04 05:08:49 -08:00
Mark Liversedge
fbd5238e4e Frame PvPf Plot
When working with smaller intervals it is difficult to see the
highlighted points when all the points are shown in black. This
patch adds a 'Frame Intervals' checkbox to enable the user to
turn off all the points when looking at specific intervals.

If no intervals are selected then this setting has no net effect.
All datapoints are shown.
2010-02-01 08:01:02 -08:00
Sean Rhea
67919e4d21 add Daniels Equivalent Power metric 2010-01-26 08:31:05 -08:00
287 changed files with 138432 additions and 4693 deletions

View File

@@ -1,2 +1,3 @@
TEMPLATE = subdirs
SUBDIRS = qwt src
CONFIG += ordered

BIN
doc/3d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -2,12 +2,16 @@
CONTENT=$(wildcard *.content)
HTML=$(subst .content,.html,$(CONTENT))
TARBALLS=$(wildcard gc_*.tgz)
OTHER=logo.jpg sample.gp sample.png cpint.gp cpint.png \
critical-power-plot.png histogram-analysis.png pf-pv-plot.png \
ride-plot.png ride-summary.png weekly-summary.png \
choose-a-cyclist.png main-window.png critical-power.png \
power.zones cyclist-info.png
OTHER= 3d.png choose-a-cyclist.png cpint.gp cpint.png critical-power-plot.png critical-power.png \
cyclist-info.png editor.png gui-preview.png histogram-analysis.png logo.jpg logo.png \
main-window.png map.png metrics-power.png metrics-timedist.png metrics-tiz.png pf-pv-plot.png \
pm.png power.zones realtime.png ride-plot.png ride-plot2.png ride-summary.png sample.gp \
sample.png weekly-summary.png google-earth.png aerolab.png
BIN= GoldenCheetah_2.0.0_Linux_x86_64.gz \
GoldenCheetah_2.0.0_Linux_x86.gz \
GoldenCheetah_2.0.0_Mac_Universal.dmg \
GoldenCheetah_2.0.0_Windows_Installer.exe
all: $(HTML)
.PHONY: all clean install
@@ -17,6 +21,10 @@ clean:
install:
rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) \
liversedge@srhea.net:/home/srhea/wwwroot/goldencheetah.org/
install-bin:
rsync -avz -e ssh $(BIN) \
srhea.net:/home/srhea/wwwroot/goldencheetah.org/
bug-tracker.html: bug-tracker.content genpage.pl
@@ -34,9 +42,15 @@ contrib.html: contrib.content genpage.pl
developers-guide.html: developers-guide.content genpage.pl
./genpage.pl "Developer's Guide" $< > $@
older-releases.html: older-releases.content genpage.pl
./genpage.pl "Older Releases" $< > $@
download.html: download.content genpage.pl
./genpage.pl "Download" $< > $@
release-notes.html: release-notes.content genpage.pl
./genpage.pl "Release Notes" $< > $@
faq.html: faq.content genpage.pl
./genpage.pl "Frequently Asked Questions" $< > $@

BIN
doc/aerolab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -22,34 +22,40 @@ other support, including:</p>
<ul>
<li>Robert Carlsen</li>
<li>Rainer Clasen</li>
<li>Chris Cleeland</li>
<li>J.T. Conklin</li>
<li>Dan Connelly</li>
<li>Damian Grauser</li>
<li>Damien Grauser</li>
<li>Steve Gribble</li>
<li>Dag Gruneau</li>
<li>Ned Harding</li>
<li>Aldy Hernandez</li>
</ul>
</td>
<td valign="top" width="33%">
<ul>
<li>Aldy Hernandez</li>
<li>Jamie Kimberley</li>
<li>Justin Knotzke</li>
<li>Andrew Kruse</li>
<li>Mark Liversedge</li>
<li>Greg Lonnon</li>
<li>Tom Montgomery</li>
<li>Eric Murray</li>
<li>Scott Overfield</li>
</ul>
</td>
<td valign="top">
<ul>
<li>Eric Murray</li>
<li>Scott Overfield</li>
<li>Mark Rages</li>
<li>Robb Romans</li>
<li>Mitsukuni Sato</li>
<li>Berend de Schouwer</li>
<li>Julian Simioni</li>
<li>Greg Steele</li>
<li>Tom Weichmann</li>
<li>Keisuke Yamaguchi</li>
</ul>
</td>
</tr>
</table>

View File

@@ -1,15 +1,53 @@
<!-- $Id: download.content,v 1.6 2009/01/09 20:45:03 rcarlsen Exp $ -->
<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.
Golden Cheetah is available in binary form for
Linux x86, Mac OS X (universal binary), and Windows.
It is also available as source code.
</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.
Golden Cheetah downloads data from all versions of the PowerTap
computer including the new Joule. If you're using the PowerTap USB cradle
(as opposed to the older, serial cable), you may need to install the
<a href="http://www.ftdichip.com/Drivers/D2XX.htm">FTDI USB driver</a>
before downloading.
</p>
<p>
On Linux and Mac OS X, Golden Cheetah also downloads from the SRM PCV. On Mac
OS X, you'll need to install <a href="http://osx-pl2303.sourceforge.net/">the
open source PL2303 driver</a> to download from an SRM.
</p>
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Download Release 2.0</strong></big>
</font>
<ul>
<li><a href="GoldenCheetah_2.0.0_Linux_x86.gz">Linux x86</a><br>
<li><a href="GoldenCheetah_2.0.0_Linux_x86_64.gz">Linux x86_64</a><br>
<li><a href="GoldenCheetah_2.0.0_Mac_Universal.dmg">Mac OS X Universal 10.5 & 10.6</a><br>
<li><a href="GoldenCheetah_2.0.0_Mac_PPC.dmg">Mac OS X 10.4</a><br>
<li><a href="GoldenCheetah_2.0.0_Windows_Installer.exe">Windows 32-bit</a>
</ul>
<p>
You can also <a href="release-notes.html">view the release notes</a> for 2.0
or <a href="older-releases.html">download older releases</a> of Golden Cheetah.
</p>
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Development Releases</strong></big>
</font>
<p>Gareth Coco has also made
<a href="http://goldencheetah.stand2surf.net/">nightly development builds</a>
available. These binaries are based on the latest code, so they have more
features and (sometimes) more bugs than the stable 2.0 release above.
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Source Code</strong></big>
@@ -21,296 +59,4 @@ The Golden Cheetah source code is available via git. See the
You can also <a href="http://github.com/srhea/GoldenCheetah/tree/master/">browse
the source on github</a>.
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Binaries</strong></big>
</font>
<p>
<center>
<table width="100%" cellspacing="5">
<tr>
<td width="15%"><i>Version</i></td>
<td width="25%"><i>Files</i></td>
<td><i>Description</i></td>
</tr>
<tr>
<td valign="top">1.2.0</td>
<td valign="top">
<a href="GoldenCheetah_1.2.0_Linux_x86.tgz">Linux x86</a><br>
<a href="GoldenCheetah_1.2.0_Linux_x86_64.tgz">Linux x86_64</a><br>
<a href="GoldenCheetah_1.2.0_Darwin_Universal.dmg">Mac OS X Universal</a><br>
<a href="GoldenCheetah_1.2.0_Windows_Installer.exe">Windows 32-bit</a>
</td>
<td valign="top">
<p>
Lots of new features in this release, including:
</p>
<ul>
<li>Direct download from SRM (R. Clasen and S. Rhea)
<li>WKO+ file import (M. Liversedge)
<li>Qollector support (M. Rages)
<li>Altitude plotting (T. Weichmann)
<li>Manual ride entry (E. Murray)
<li>Power zones shading (D. Connell)
<li>Weekly summary histograms (R. Carlsen)
<li>Automatic CP estimation from CP graph (D. Connell)
<li>Support for running off a USB stick (J. Knotzke)
<li>OS-specific directory layout (J. Simioni)
<li>PF/PV plot improvements (B. de Schouwer)
<li>Memory leak fixes (G. Lonnon)
</ul>
<p>Thanks also to Jamie Kimberley for extensive testing.
</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
X PowerPC</a></td>
<td valign="top">Adds the Power Histogram, which shows how much time a rider
spent at each particular power level during a ride.</td>
</tr>
<tr>
<td valign="top">Sep 19, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-09-19_Darwin_powerpc.dmg">Mac OS
X PowerPC</a></td>
<td valign="top">Adds the Critical Power Plot, which shows the highest average
power you've achieved for every interval length over all your rides and the
selected ride. Also shows download progress in minutes of ride data
downloaded.</td>
</tr>
<tr>
<td valign="top">Sep 7, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-09-07_Darwin_powerpc.dmg">Mac OS
X PowerPC</a></td>
<td valign="top">Adds speed and cadence to the ride plot. Fixes a bug
found by George Gilliland where reseting the time during a ride could cause
the GUI to crash.</td>
</tr>
<tr>
<td valign="top">Sep 6, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-09-06_Darwin_powerpc.dmg">Mac OS
X PowerPC</a></td>
<td valign="top">The first release of the Golden Cheetah GUI.</td>
</tr>
</table>
</center>
<p>
<hr width="50%">
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Older Stuff</strong></big>
</font>
<p>
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%" cellspacing="10">
<tr>
<td width="20%"><i>Date</i></td>
<td width="30%"><i>File</i></td>
<td><i>Description</i></td>
</tr>
<tr>
<td valign="top">Aug 11, 2006</td>
<td valign="top"><a href="gc_2006-08-11.tgz">gc_2006-08-11.tgz</a></td>
<td valign="top">ptdl now works with Keyspan USB-to-serial adaptor, after
debugging help from Rob Carlsen.
</td>
</tr>
<tr>
<td valign="top">May 27, 2006</td>
<td valign="top"><a href="gc_2006-05-27.tgz">gc_2006-05-27.tgz</a></td>
<td valign="top">Adds the <code>cpint</code> program for computing critical
power intervals and the <code>ptpk</code> program for converting from
PowerTuned data files (see the <a href="users-guide.html">User's
Guide</a>).</td>
</tr>
<tr>
<td valign="top">May 16, 2006</td>
<td valign="top"><a href="gc_2006-05-16.tgz">gc_2006-05-16.tgz</a></td>
<td valign="top">The first code release, containing <code>ptdl</code> and
<code>ptunpk</code>.</td>
</tr>
</table>
</center>

BIN
doc/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -55,6 +55,7 @@ body {
<p> <b><a href="index.html">Introduction</a></b>
<br> <b><a href="screenshots.html">Screenshots</a>
<br> <b><a href="http://bugs.goldencheetah.org/projects/goldencheetah/wiki">Wiki</a>
<br> <b><a href="users-guide.html">User's Guide</a>
<br> <b><a href="developers-guide.html">Developer's Guide</a>
<br> <b><a href="faq.html">FAQ</a>

BIN
doc/google-earth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

View File

@@ -13,7 +13,8 @@ GoldenCheetah is a software package that:
<ul>
<li>Downloads ride data directly from the CycleOps PowerTap and the SRM
PowerControl.<p>
PowerControl V. Support for SRM PowerControl VI and VII is planned for the
future.<p>
<li>Imports ride data downloaded with other programs, including TrainingPeaks
WKO+ and the manufacturers' software for the Ergomo, Garmin, Polar, PowerTap,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 159 KiB

BIN
doc/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

BIN
doc/metrics-power.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
doc/metrics-timedist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
doc/metrics-tiz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

399
doc/older-releases.content Normal file
View File

@@ -0,0 +1,399 @@
<p>
This page contains older releases of Golden Cheetah. For the latest version,
please see <a href="download.html">the download page</a> instead.
</p>
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Golden Cheetah</strong></big>
</font>
<p>
<center>
<table width="100%" cellspacing="5">
<tr>
<td width="15%"><i>Version</i></td>
<td width="25%"><i>Files</i></td>
<td><i>Description</i></td>
</tr>
<tr>
<td valign="top">1.3.0</td>
<td valign="top">
<a href="GoldenCheetah_1.3.0_Linux_x86.gz">Linux x86</a><br>
<a href="GoldenCheetah_1.3.0_Linux_x86_64.gz">Linux x86_64</a><br>
<a href="GoldenCheetah_1.3.0_Mac_Universal.zip">Mac OS X Universal</a><br>
<a href="GoldenCheetah_1.3.0_Windows_Installer.exe">Windows 32-bit</a>
</td>
<td valign="top">
<p>
Lots of new features:
<p>
Realtime Mode:
<ul>
<li>Graph data as you ride (Mark Liversedge, Justin Knotzke, Steve Gribble)</li>
</ul>
</p>
<p>
Charts:
<ul>
<li>Added Performance Manager (Eric Murray)</li>
<li>Added 3D Modeling (Mark Liversedge and Greg Steele)</li>
<li>Up to four y-axes on Ride Plot (Sean Rhea)</li>
<li>Option to show work instead of power in Critical Power Plot (Sean Rhea)</li>
</ul>
</p>
<p>
Intervals:
<ul>
<li>Configurable metrics for intervals (Sean Rhea)</li>
<li>Find peak powers and add to intervals (Mark Liversedge)</li>
<li>Highlight intervals in plots (Damien Grauser)</li>
</ul>
</p>
<p>
Device support:
<ul>
<li>Serial port support on Windows (Mark Liversedge)</li>
<li>Erase SRM memory without downloading (Sean Rhea)</li>
</ul>
</p>
<p>
Imports:
<ul>
<li>New ride import wizard (Mark Liversedge, Jamie Kimberley)</li>
<li>Support Computrainer 3dp file format (Greg Lonnon)</li>
<li>Support WKO v3 file format (Mark Liversedge)</li>
<li>Support files with Garmin "smart recording" (Greg Lonnon)</li>
<li>New GoldenCheetah (.gc) file format (Sean Rhea)</li>
</ul>
</p>
<p>
New/improved ride metrics:
<ul>
<li>Added Joe Friel's Aerobic Decoupling (Sean Rhea)</li>
<li>Added training points system by running coach Jack Daniels (Sean Rhea)</li>
<li>Better elevation gain estimates (Sean Rhea)</li>
</ul>
</p>
<p>
Support for more languages:
<ul>
<li>French (Damien Grauser)</li>
<li>Japanese (Mitsukuni Sato, Keisuke Yamaguchi)</li>
</ul>
</p>
<p>
Other new features:
<ul>
<li>Group rides into seasons (Justin Knotzke)</li>
<li>Better ride calendar (Berend De Schouwer)</li>
<li>Ride list pop-up menu (Thomas Weichmann)</li>
</ul>
</p>
</p>
<ul>
<li>Direct download from SRM (R. Clasen and S. Rhea)
<li>WKO+ file import (M. Liversedge)
<li>Qollector support (M. Rages)
<li>Altitude plotting (T. Weichmann)
<li>Manual ride entry (E. Murray)
<li>Power zones shading (D. Connell)
<li>Weekly summary histograms (R. Carlsen)
<li>Automatic CP estimation from CP graph (D. Connell)
<li>Support for running off a USB stick (J. Knotzke)
<li>OS-specific directory layout (J. Simioni)
<li>PF/PV plot improvements (B. de Schouwer)
<li>Memory leak fixes (G. Lonnon)
</ul>
<p>Thanks also to Jamie Kimberley for extensive testing.
</td>
</tr>
<tr>
<td valign="top">1.2.0</td>
<td valign="top">
<a href="GoldenCheetah_1.2.0_Linux_x86.tgz">Linux x86</a><br>
<a href="GoldenCheetah_1.2.0_Linux_x86_64.tgz">Linux x86_64</a><br>
<a href="GoldenCheetah_1.2.0_Darwin_Universal.dmg">Mac OS X Universal</a><br>
<a href="GoldenCheetah_1.2.0_Windows_Installer.exe">Windows 32-bit</a>
</td>
<td valign="top">
<p>
Lots of new features in this release, including:
</p>
<ul>
<li>Direct download from SRM (R. Clasen and S. Rhea)
<li>WKO+ file import (M. Liversedge)
<li>Qollector support (M. Rages)
<li>Altitude plotting (T. Weichmann)
<li>Manual ride entry (E. Murray)
<li>Power zones shading (D. Connell)
<li>Weekly summary histograms (R. Carlsen)
<li>Automatic CP estimation from CP graph (D. Connell)
<li>Support for running off a USB stick (J. Knotzke)
<li>OS-specific directory layout (J. Simioni)
<li>PF/PV plot improvements (B. de Schouwer)
<li>Memory leak fixes (G. Lonnon)
</ul>
<p>Thanks also to Jamie Kimberley for extensive testing.
</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
X PowerPC</a></td>
<td valign="top">Adds the Power Histogram, which shows how much time a rider
spent at each particular power level during a ride.</td>
</tr>
<tr>
<td valign="top">Sep 19, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-09-19_Darwin_powerpc.dmg">Mac OS
X PowerPC</a></td>
<td valign="top">Adds the Critical Power Plot, which shows the highest average
power you've achieved for every interval length over all your rides and the
selected ride. Also shows download progress in minutes of ride data
downloaded.</td>
</tr>
<tr>
<td valign="top">Sep 7, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-09-07_Darwin_powerpc.dmg">Mac OS
X PowerPC</a></td>
<td valign="top">Adds speed and cadence to the ride plot. Fixes a bug
found by George Gilliland where reseting the time during a ride could cause
the GUI to crash.</td>
</tr>
<tr>
<td valign="top">Sep 6, 2006</td>
<td valign="top"><a href="GoldenCheetah_2006-09-06_Darwin_powerpc.dmg">Mac OS
X PowerPC</a></td>
<td valign="top">The first release of the Golden Cheetah GUI.</td>
</tr>
</table>
</center>
<p>
<hr width="50%">
<p>
<font face="arial,helvetica,sanserif">
<big><strong>Older Stuff</strong></big>
</font>
<p>
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%" cellspacing="10">
<tr>
<td width="20%"><i>Date</i></td>
<td width="30%"><i>File</i></td>
<td><i>Description</i></td>
</tr>
<tr>
<td valign="top">Aug 11, 2006</td>
<td valign="top"><a href="gc_2006-08-11.tgz">gc_2006-08-11.tgz</a></td>
<td valign="top">ptdl now works with Keyspan USB-to-serial adaptor, after
debugging help from Rob Carlsen.
</td>
</tr>
<tr>
<td valign="top">May 27, 2006</td>
<td valign="top"><a href="gc_2006-05-27.tgz">gc_2006-05-27.tgz</a></td>
<td valign="top">Adds the <code>cpint</code> program for computing critical
power intervals and the <code>ptpk</code> program for converting from
PowerTuned data files (see the <a href="users-guide.html">User's
Guide</a>).</td>
</tr>
<tr>
<td valign="top">May 16, 2006</td>
<td valign="top"><a href="gc_2006-05-16.tgz">gc_2006-05-16.tgz</a></td>
<td valign="top">The first code release, containing <code>ptdl</code> and
<code>ptunpk</code>.</td>
</tr>
</table>
</center>

BIN
doc/pm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
doc/realtime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

81
doc/release-notes.content Normal file
View File

@@ -0,0 +1,81 @@
<p>
<font face="arial,helvetica,sanserif">
<big><strong>GoldenCheetah 2.0</strong></big>
</font>
</p>
<p>
New Features
<ul>
<li>Aerolab (Andy Froncioni)</li>
<li>View ride in Google Maps (Greg Lonnon)</li>
<li>Long Term Metrics (Mark Liversedge)</li>
<li>User configurable ride metadata (Mark Liversedge)</li>
<li>Ride editor and tools (Mark Liversedge)</li>
<li>HR Zones and TRIMP Metrics (Damien Grauser)</li>
<li>Twitter support (Justin Knotzke)</li>
</ul>
</p>
<p>
Internationalisation
<ul>
<li>Updates to French translation (Damien Grauser)</li>
<li>Japanese translation (Mitsukuni Sato)</li>
</ul>
</p>
<p>
New Logo and Icons
<ul>
<li>Golden Cheetah Logo(Dan Schmalz)</li>
</ul>
</p>
<p>
Enhanced Ride Plot
<ul>
<li>Ride plot stacked view (Damien Grauser)</li>
<li>Scrolling Ride Plot (Mark Liversedge)</li>
</ul>
</p>
<p>
New Devices and File Formats Supported
<ul>
<li>Support for Joule BIN File Format (Damien Grauser)</li>
<li>Tacx CAF Ride File Format Support (Ilja Booij)</li>
<li>Garmin FIT ride file support (Sean Rhea)</li>
<li>Export to Google Earth 5.2 KML (Mark Liversedge)</li>
<li>Training Peaks PWX ride file support (Mark Liversedge)</li>
<li>Polar SRD ride file support (Mark Liversedge)</li>
<li>Racermate CompCS/Ergvideo .TXT ride file support (Mark Liversedge)</li>
</ul>
<p>
<p>
Numerous enhancements and bug fixes from
<ul>
<li>Julian Baumgartner</li>
<li>Robert Carlsen</li>
<li>Rainer Clasen</li>
<li>Gareth Coco</li>
<li>Dag Gruneau</li>
<li>Jamie Kimberley</li>
<li>Jim Ley</li>
<li>Patrick J. McNerthney</li>
<li>Austin Roach</li>
<li>Ken Sallot</li>
<li>Thomas Weichmann</li>
</ul>
</p>
<p>
Builds, testing and support
<ul>
<li>Robert Carlsen</li>
<li>Gareth Coco</li>
<li>Jamie Kimberley</li>
<li>Justin Knotzke</li>
</ul>
</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 192 KiB

BIN
doc/ride-plot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -17,6 +17,13 @@ Plotting Altitude, Cadence, Heart Rate, Power, and Speed
<p>
<img src="ride-plot.png" alt="Power and HR Plot" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Plotting in a Stacked View
</font></big>
<p>
<img src="ride-plot2.png" alt="Stacked Power and HR Plot" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Plotting Critical Power
@@ -45,6 +52,66 @@ The Weekly Summary
<p>
<img src="weekly-summary.png" alt="The Weekly Summary" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Plot from a selection of over 30 metrics
</font></big>
<p>
<img src="metrics-power.png" alt="The Power Metrics" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Including Time and Distance
</font></big>
<p>
<img src="metrics-timedist.png" alt="The Time and Distance" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Time In Zone
</font></big>
<p>
<img src="metrics-tiz.png" alt="The Time In Zone" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Plot with Google Maps
</font></big>
<p>
<img src="map.png" alt="Google Maps" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Plot in 3 Dimensions
</font></big>
<p>
<img src="3d.png" alt="The 3d plot" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Edit and Correct Ride Data
</font></big>
<p>
<img src="editor.png" alt="The Ride Editor" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
The Performance Manager
</font></big>
<p>
<img src="pm.png" alt="The Performance Manager" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Train with a Computrainer or ANT+ Device
</font></big>
<p>
<img src="realtime.png" alt="The Realtime Window" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
Export to Google Earth 5.2
</font></big>
<p>
<img src="google-earth.png" alt="Google Earth 5.2" align="center">
<p>
<big><font face="arial,helvetica,sanserif">
The Aerolab
</font></big>
<p>
<img src="aerolab.png" alt="Aerolab" align="center">
</center>

View File

@@ -1,17 +1,35 @@
<!-- $Id: users-guide.content,v 1.5 2006/05/27 16:32:46 srhea Exp $ -->
<p>
Note that more detailed information is often available on the
<a href = http://bugs.goldencheetah.org/projects/goldencheetah/wiki>
Golden Cheetah Wiki</a>.
<p>
What follows is a brief step-by-step guide to installing and setting up
Golden Cheetah.
<p>
<big><font face="arial,helvetica,sanserif">
Step 1: Installing the FTDI drivers
Step 1 (optional): Installing the FTDI drivers
</font></big>
<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.
(Note: version 1.7 of the FTDI drivers for Mac seems to be buggy. Until they
post a patched version, you can download version 1.6
<a href="http://bugs.goldencheetah.org/attachments/download/1/Universal_D2XX0.1.6.dmg">here</a>.)
</p>
This step is only needed if you want to download rides from a powertap
pro/comp/cervo head unit via the supplied USB cradle. Furthermore, most
Windows and Linux systems should recognize the device without installing
the drivers below.
<p>
Depending on your operating system, you <i>may</i> need to install the
<ahref="http://www.ftdichip.com/Drivers/D2XX.htm">FTDI D2XX driver</a> if
you're using the PowerTap's new USB download cradle.
Note: version 0.1.7 of the FTDI drivers for Mac seems to be buggy. Until they
post a patched version, you can download version 0.1.6
<a href="http://bugs.goldencheetah.org/attachments/download/1/Universal_D2XX0.1.6.dmg"> here</a>
and install via the terminal. Or if you are not terminal savvy, download an installer that will
perform the installation of the 0.1.6 drivers for you
<a href="http://bugs.goldencheetah.org/attachments/download/248/Install_D2XX_drivers.mpkg.zip">
here</a>.
<p>
If you're running Linux, you may also need to uninstall the <code>brtty</code>
@@ -46,20 +64,20 @@ 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:
The Linux version of GoldenCheetah is distributed as a GZipped archive.
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
gunzip -vf GoldenCheetah_X.X.X_Linux_x86.gz
cd GoldenCheetah_X.X.X_Linux_x86
sudo cp GoldenCheetah /usr/local/bin
cd ..
rm -rf GoldenCheetah_DATE_Linux_x86.tgz
rm -rf GoldenCheetah_X.X.X_Linux_x86.gz
</pre>
Be sure to replace "DATE" with the date of the revision you downloaded, such
as "2007-09-23".
Be sure to replace "X.X.X" with the version of the release you downloaded,
such as "2.0.0".
<p>
<big><font face="arial,helvetica,sanserif">
@@ -231,7 +249,7 @@ based on percentages of your CP value. The zones are:
<td>150%</td>
</tr>
<tr>
<td>Z1</td>
<td>Z7</td>
<td>Neuromuscular</td>
<td>150%</td>
<td>MAX</td>

View File

@@ -8,18 +8,9 @@ tracker</a>. Instructions for doing so are <a href="bug-tracker.html">here</a>.
<p>
Examples of some features are:
<ul>
<li>Graph ride metrics (daily hours, work, BikeScore) over the long
term (weeks, seasons)</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>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>Show mulitple rides (seasons, etc.) in power histogram</li>
<li>Annotate ride plot</li>
<li>Label rides by type, course</li>

View File

@@ -114,7 +114,7 @@ CONFIG += QwtWidgets
# Otherwise you have to build it from the designer directory.
######################################################################
CONFIG += QwtDesigner
#CONFIG += QwtDesigner
######################################################################
# If you want to auto build the examples, enable the line below

207
qxt/src/qxtglobal.h Normal file
View File

@@ -0,0 +1,207 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtCore module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTGLOBAL_H
#define QXTGLOBAL_H
#include <QtGlobal>
#define QXT_VERSION 0x000700
#define QXT_VERSION_STR "0.7.0"
//--------------------------global macros------------------------------
#ifndef QXT_NO_MACROS
#endif // QXT_NO_MACROS
//--------------------------export macros------------------------------
#define QXT_DLLEXPORT DO_NOT_USE_THIS_ANYMORE
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_CORE)
# define QXT_CORE_EXPORT Q_DECL_EXPORT
# else
# define QXT_CORE_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_CORE_EXPORT
#endif // BUILD_QXT_CORE
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_GUI)
# define QXT_GUI_EXPORT Q_DECL_EXPORT
# else
# define QXT_GUI_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_GUI_EXPORT
#endif // BUILD_QXT_GUI
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_NETWORK)
# define QXT_NETWORK_EXPORT Q_DECL_EXPORT
# else
# define QXT_NETWORK_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_NETWORK_EXPORT
#endif // BUILD_QXT_NETWORK
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_SQL)
# define QXT_SQL_EXPORT Q_DECL_EXPORT
# else
# define QXT_SQL_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_SQL_EXPORT
#endif // BUILD_QXT_SQL
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_WEB)
# define QXT_WEB_EXPORT Q_DECL_EXPORT
# else
# define QXT_WEB_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_WEB_EXPORT
#endif // BUILD_QXT_WEB
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_BERKELEY)
# define QXT_BERKELEY_EXPORT Q_DECL_EXPORT
# else
# define QXT_BERKELEY_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_BERKELEY_EXPORT
#endif // BUILD_QXT_BERKELEY
#if !defined(QXT_STATIC)
# if defined(BUILD_QXT_ZEROCONF)
# define QXT_ZEROCONF_EXPORT Q_DECL_EXPORT
# else
# define QXT_ZEROCONF_EXPORT Q_DECL_IMPORT
# endif
#else
# define QXT_ZEROCONF_EXPORT
#endif // QXT_ZEROCONF_EXPORT
#if defined BUILD_QXT_CORE || defined BUILD_QXT_GUI || defined BUILD_QXT_SQL || defined BUILD_QXT_NETWORK || defined BUILD_QXT_WEB || defined BUILD_QXT_BERKELEY || defined BUILD_QXT_ZEROCONF
# define BUILD_QXT
#endif
QXT_CORE_EXPORT const char* qxtVersion();
#ifndef QT_BEGIN_NAMESPACE
#define QT_BEGIN_NAMESPACE
#endif
#ifndef QT_END_NAMESPACE
#define QT_END_NAMESPACE
#endif
#ifndef QT_FORWARD_DECLARE_CLASS
#define QT_FORWARD_DECLARE_CLASS(Class) class Class;
#endif
/****************************************************************************
** This file is derived from code bearing the following notice:
** The sole author of this file, Adam Higerd, has explicitly disclaimed all
** copyright interest and protection for the content within. This file has
** been placed in the public domain according to United States copyright
** statute and case law. In jurisdictions where this public domain dedication
** is not legally recognized, anyone who receives a copy of this file is
** permitted to use, modify, duplicate, and redistribute this file, in whole
** or in part, with no restrictions or conditions. In these jurisdictions,
** this file shall be copyright (C) 2006-2008 by Adam Higerd.
****************************************************************************/
#define QXT_DECLARE_PRIVATE(PUB) friend class PUB##Private; QxtPrivateInterface<PUB, PUB##Private> qxt_d;
#define QXT_DECLARE_PUBLIC(PUB) friend class PUB;
#define QXT_INIT_PRIVATE(PUB) qxt_d.setPublic(this);
#define QXT_D(PUB) PUB##Private& d = qxt_d()
#define QXT_P(PUB) PUB& p = qxt_p()
template <typename PUB>
class QxtPrivate
{
public:
virtual ~QxtPrivate()
{}
inline void QXT_setPublic(PUB* pub)
{
qxt_p_ptr = pub;
}
protected:
inline PUB& qxt_p()
{
return *qxt_p_ptr;
}
inline const PUB& qxt_p() const
{
return *qxt_p_ptr;
}
private:
PUB* qxt_p_ptr;
};
template <typename PUB, typename PVT>
class QxtPrivateInterface
{
friend class QxtPrivate<PUB>;
public:
QxtPrivateInterface()
{
pvt = new PVT;
}
~QxtPrivateInterface()
{
delete pvt;
}
inline void setPublic(PUB* pub)
{
pvt->QXT_setPublic(pub);
}
inline PVT& operator()()
{
return *static_cast<PVT*>(pvt);
}
inline const PVT& operator()() const
{
return *static_cast<PVT*>(pvt);
}
private:
QxtPrivateInterface(const QxtPrivateInterface&) { }
QxtPrivateInterface& operator=(const QxtPrivateInterface&) { }
QxtPrivate<PUB>* pvt;
};
#endif // QXT_GLOBAL

106
qxt/src/qxtnamespace.h Normal file
View File

@@ -0,0 +1,106 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtCore module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTNAMESPACE_H
#define QXTNAMESPACE_H
#include <qxtglobal.h>
#if (defined BUILD_QXT | defined Q_MOC_RUN) && !defined(QXT_DOXYGEN_RUN)
#include <QObject>
class QXT_CORE_EXPORT Qxt : public QObject
{
Q_OBJECT
Q_ENUMS(Rotation)
Q_ENUMS(DecorationStyle)
Q_ENUMS(ErrorCode)
public:
#else
namespace Qxt
{
#endif
enum Rotation
{
NoRotation = 0,
UpsideDown = 180,
Clockwise = 90,
CounterClockwise = 270
};
enum DecorationStyle
{
NoDecoration,
Buttonlike,
Menulike
};
enum ErrorCode
{
NoError,
UnknownError,
LogicalError,
Bug,
UnexpectedEndOfFunction,
NotImplemented,
CodecError,
NotInitialised,
EndOfFile,
FileIOError,
FormatError,
DeviceError,
SDLError,
InsufficientMemory,
SeeErrorString,
UnexpectedNullParameter,
ClientTimeout,
SocketIOError,
ParserError,
HeaderTooLong,
Auth,
Overflow
};
enum QxtItemDataRole
{
ItemStartTimeRole = Qt::UserRole + 1,
ItemDurationRole = ItemStartTimeRole + 1,
UserRole = ItemDurationRole + 23
};
enum Timeunit
{
Second,
Minute,
Hour,
Day,
Week,
Month,
Year
};
};
#endif // QXTNAMESPACE_H

748
qxt/src/qxtspanslider.cpp Normal file
View File

@@ -0,0 +1,748 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtGui module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#include "qxtspanslider.h"
#include "qxtspanslider_p.h"
#include <QKeyEvent>
#include <QMouseEvent>
#include <QApplication>
#include <QStylePainter>
#include <QStyleOptionSlider>
QxtSpanSliderPrivate::QxtSpanSliderPrivate() :
lower(0),
upper(0),
lowerPos(0),
upperPos(0),
offset(0),
position(0),
lastPressed(QxtSpanSlider::NoHandle),
mainControl(QxtSpanSlider::LowerHandle),
lowerPressed(QStyle::SC_None),
upperPressed(QStyle::SC_None),
movement(QxtSpanSlider::FreeMovement),
firstMovement(false),
blockTracking(false)
{
}
void QxtSpanSliderPrivate::initStyleOption(QStyleOptionSlider* option, QxtSpanSlider::SpanHandle handle) const
{
const QxtSpanSlider* p = &qxt_p();
p->initStyleOption(option);
option->sliderPosition = (handle == QxtSpanSlider::LowerHandle ? lowerPos : upperPos);
option->sliderValue = (handle == QxtSpanSlider::LowerHandle ? lower : upper);
}
int QxtSpanSliderPrivate::pixelPosToRangeValue(int pos) const
{
QStyleOptionSlider opt;
initStyleOption(&opt);
int sliderMin = 0;
int sliderMax = 0;
int sliderLength = 0;
const QSlider* p = &qxt_p();
const QRect gr = p->style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, p);
const QRect sr = p->style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, p);
if (p->orientation() == Qt::Horizontal)
{
sliderLength = sr.width();
sliderMin = gr.x();
sliderMax = gr.right() - sliderLength + 1;
}
else
{
sliderLength = sr.height();
sliderMin = gr.y();
sliderMax = gr.bottom() - sliderLength + 1;
}
return QStyle::sliderValueFromPosition(p->minimum(), p->maximum(), pos - sliderMin,
sliderMax - sliderMin, opt.upsideDown);
}
void QxtSpanSliderPrivate::handleMousePress(const QPoint& pos, QStyle::SubControl& control, int value, QxtSpanSlider::SpanHandle handle)
{
QStyleOptionSlider opt;
initStyleOption(&opt, handle);
QxtSpanSlider* p = &qxt_p();
const QStyle::SubControl oldControl = control;
control = p->style()->hitTestComplexControl(QStyle::CC_Slider, &opt, pos, p);
const QRect sr = p->style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, p);
if (control == QStyle::SC_SliderHandle)
{
position = value;
offset = pick(pos - sr.topLeft());
lastPressed = handle;
p->setSliderDown(true);
emit p->sliderPressed(handle);
}
if (control != oldControl)
p->update(sr);
}
void QxtSpanSliderPrivate::setupPainter(QPainter* painter, Qt::Orientation orientation, qreal x1, qreal y1, qreal x2, qreal y2) const
{
QColor highlight = qxt_p().palette().color(QPalette::Highlight);
QLinearGradient gradient(x1, y1, x2, y2);
gradient.setColorAt(0, highlight.dark(120));
gradient.setColorAt(1, highlight.light(108));
painter->setBrush(gradient);
if (orientation == Qt::Horizontal)
painter->setPen(QPen(highlight.dark(130), 0));
else
painter->setPen(QPen(highlight.dark(150), 0));
}
void QxtSpanSliderPrivate::drawSpan(QStylePainter* painter, const QRect& rect) const
{
QStyleOptionSlider opt;
initStyleOption(&opt);
const QSlider* p = &qxt_p();
// area
QRect groove = p->style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, p);
if (opt.orientation == Qt::Horizontal)
groove.adjust(0, 0, -1, 0);
else
groove.adjust(0, 0, 0, -1);
// pen & brush
painter->setPen(QPen(p->palette().color(QPalette::Dark).light(110), 0));
if (opt.orientation == Qt::Horizontal)
setupPainter(painter, opt.orientation, groove.center().x(), groove.top(), groove.center().x(), groove.bottom());
else
setupPainter(painter, opt.orientation, groove.left(), groove.center().y(), groove.right(), groove.center().y());
// draw groove
painter->drawRect(rect.intersected(groove));
}
void QxtSpanSliderPrivate::drawHandle(QStylePainter* painter, QxtSpanSlider::SpanHandle handle) const
{
QStyleOptionSlider opt;
initStyleOption(&opt, handle);
opt.subControls = QStyle::SC_SliderHandle;
QStyle::SubControl pressed = (handle == QxtSpanSlider::LowerHandle ? lowerPressed : upperPressed);
if (pressed == QStyle::SC_SliderHandle)
{
opt.activeSubControls = pressed;
opt.state |= QStyle::State_Sunken;
}
painter->drawComplexControl(QStyle::CC_Slider, opt);
}
void QxtSpanSliderPrivate::triggerAction(QAbstractSlider::SliderAction action, bool main)
{
int value = 0;
bool no = false;
bool up = false;
const int min = qxt_p().minimum();
const int max = qxt_p().maximum();
const QxtSpanSlider::SpanHandle altControl = (mainControl == QxtSpanSlider::LowerHandle ? QxtSpanSlider::UpperHandle : QxtSpanSlider::LowerHandle);
blockTracking = true;
switch (action)
{
case QAbstractSlider::SliderSingleStepAdd:
if ((main && mainControl == QxtSpanSlider::UpperHandle) || (!main && altControl == QxtSpanSlider::UpperHandle))
{
value = qBound(min, upper + qxt_p().singleStep(), max);
up = true;
break;
}
value = qBound(min, lower + qxt_p().singleStep(), max);
break;
case QAbstractSlider::SliderSingleStepSub:
if ((main && mainControl == QxtSpanSlider::UpperHandle) || (!main && altControl == QxtSpanSlider::UpperHandle))
{
value = qBound(min, upper - qxt_p().singleStep(), max);
up = true;
break;
}
value = qBound(min, lower - qxt_p().singleStep(), max);
break;
case QAbstractSlider::SliderToMinimum:
value = min;
if ((main && mainControl == QxtSpanSlider::UpperHandle) || (!main && altControl == QxtSpanSlider::UpperHandle))
up = true;
break;
case QAbstractSlider::SliderToMaximum:
value = max;
if ((main && mainControl == QxtSpanSlider::UpperHandle) || (!main && altControl == QxtSpanSlider::UpperHandle))
up = true;
break;
case QAbstractSlider::SliderMove:
if ((main && mainControl == QxtSpanSlider::UpperHandle) || (!main && altControl == QxtSpanSlider::UpperHandle))
up = true;
case QAbstractSlider::SliderNoAction:
no = true;
break;
default:
qWarning("QxtSpanSliderPrivate::triggerAction: Unknown action");
break;
}
if (!no && !up)
{
if (movement == QxtSpanSlider::NoCrossing)
value = qMin(value, upper);
else if (movement == QxtSpanSlider::NoOverlapping)
value = qMin(value, upper - 1);
if (movement == QxtSpanSlider::FreeMovement && value > upper)
{
swapControls();
qxt_p().setUpperPosition(value);
}
else
{
qxt_p().setLowerPosition(value);
}
}
else if (!no)
{
if (movement == QxtSpanSlider::NoCrossing)
value = qMax(value, lower);
else if (movement == QxtSpanSlider::NoOverlapping)
value = qMax(value, lower + 1);
if (movement == QxtSpanSlider::FreeMovement && value < lower)
{
swapControls();
qxt_p().setLowerPosition(value);
}
else
{
qxt_p().setUpperPosition(value);
}
}
blockTracking = false;
qxt_p().setLowerValue(lowerPos);
qxt_p().setUpperValue(upperPos);
}
void QxtSpanSliderPrivate::swapControls()
{
qSwap(lower, upper);
qSwap(lowerPressed, upperPressed);
lastPressed = (lastPressed == QxtSpanSlider::LowerHandle ? QxtSpanSlider::UpperHandle : QxtSpanSlider::LowerHandle);
mainControl = (mainControl == QxtSpanSlider::LowerHandle ? QxtSpanSlider::UpperHandle : QxtSpanSlider::LowerHandle);
}
void QxtSpanSliderPrivate::updateRange(int min, int max)
{
Q_UNUSED(min);
Q_UNUSED(max);
// setSpan() takes care of keeping span in range
qxt_p().setSpan(lower, upper);
}
void QxtSpanSliderPrivate::movePressedHandle()
{
switch (lastPressed)
{
case QxtSpanSlider::LowerHandle:
if (lowerPos != lower)
{
bool main = (mainControl == QxtSpanSlider::LowerHandle);
triggerAction(QAbstractSlider::SliderMove, main);
}
break;
case QxtSpanSlider::UpperHandle:
if (upperPos != upper)
{
bool main = (mainControl == QxtSpanSlider::UpperHandle);
triggerAction(QAbstractSlider::SliderMove, main);
}
break;
default:
break;
}
}
/*!
\class QxtSpanSlider
\inmodule QxtGui
\brief The QxtSpanSlider widget is a QSlider with two handles.
QxtSpanSlider is a slider with two handles. QxtSpanSlider is
handy for letting user to choose an span between min/max.
The span color is calculated based on QPalette::Highlight.
The keys are bound according to the following table:
\table
\header \o Orientation \o Key \o Handle
\row \o Qt::Horizontal \o Qt::Key_Left \o lower
\row \o Qt::Horizontal \o Qt::Key_Right \o lower
\row \o Qt::Horizontal \o Qt::Key_Up \o upper
\row \o Qt::Horizontal \o Qt::Key_Down \o upper
\row \o Qt::Vertical \o Qt::Key_Up \o lower
\row \o Qt::Vertical \o Qt::Key_Down \o lower
\row \o Qt::Vertical \o Qt::Key_Left \o upper
\row \o Qt::Vertical \o Qt::Key_Right \o upper
\endtable
Keys are bound by the time the slider is created. A key is bound
to same handle for the lifetime of the slider. So even if the handle
representation might change from lower to upper, the same key binding
remains.
\image qxtspanslider.png "QxtSpanSlider in Plastique style."
\bold {Note:} QxtSpanSlider inherits QSlider for implementation specific
reasons. Adjusting any single handle specific properties like
\list
\o QAbstractSlider::sliderPosition
\o QAbstractSlider::value
\endlist
has no effect. However, all slider specific properties like
\list
\o QAbstractSlider::invertedAppearance
\o QAbstractSlider::invertedControls
\o QAbstractSlider::minimum
\o QAbstractSlider::maximum
\o QAbstractSlider::orientation
\o QAbstractSlider::pageStep
\o QAbstractSlider::singleStep
\o QSlider::tickInterval
\o QSlider::tickPosition
\endlist
are taken into consideration.
*/
/*!
\enum QxtSpanSlider::HandleMovementMode
This enum describes the available handle movement modes.
\value FreeMovement The handles can be moved freely.
\value NoCrossing The handles cannot cross, but they can still overlap each other. The lower and upper values can be the same.
\value NoOverlapping The handles cannot overlap each other. The lower and upper values cannot be the same.
*/
/*!
\enum QxtSpanSlider::SpanHandle
This enum describes the available span handles.
\omitvalue NoHandle \omit Internal only (for now). \endomit
\value LowerHandle The lower boundary handle.
\value UpperHandle The upper boundary handle.
*/
/*!
\fn QxtSpanSlider::lowerValueChanged(int lower)
This signal is emitted whenever the \a lower value has changed.
*/
/*!
\fn QxtSpanSlider::upperValueChanged(int upper)
This signal is emitted whenever the \a upper value has changed.
*/
/*!
\fn QxtSpanSlider::spanChanged(int lower, int upper)
This signal is emitted whenever both the \a lower and the \a upper
values have changed ie. the span has changed.
*/
/*!
\fn QxtSpanSlider::lowerPositionChanged(int lower)
This signal is emitted whenever the \a lower position has changed.
*/
/*!
\fn QxtSpanSlider::upperPositionChanged(int upper)
This signal is emitted whenever the \a upper position has changed.
*/
/*!
\fn QxtSpanSlider::sliderPressed(SpanHandle handle)
This signal is emitted whenever the \a handle has been pressed.
*/
/*!
Constructs a new QxtSpanSlider with \a parent.
*/
QxtSpanSlider::QxtSpanSlider(QWidget* parent) : QSlider(parent)
{
QXT_INIT_PRIVATE(QxtSpanSlider);
connect(this, SIGNAL(rangeChanged(int, int)), &qxt_d(), SLOT(updateRange(int, int)));
connect(this, SIGNAL(sliderReleased()), &qxt_d(), SLOT(movePressedHandle()));
}
/*!
Constructs a new QxtSpanSlider with \a orientation and \a parent.
*/
QxtSpanSlider::QxtSpanSlider(Qt::Orientation orientation, QWidget* parent) : QSlider(orientation, parent)
{
QXT_INIT_PRIVATE(QxtSpanSlider);
connect(this, SIGNAL(rangeChanged(int, int)), &qxt_d(), SLOT(updateRange(int, int)));
connect(this, SIGNAL(sliderReleased()), &qxt_d(), SLOT(movePressedHandle()));
}
/*!
Destructs the span slider.
*/
QxtSpanSlider::~QxtSpanSlider()
{
}
/*!
\property QxtSpanSlider::handleMovementMode
\brief the handle movement mode
*/
QxtSpanSlider::HandleMovementMode QxtSpanSlider::handleMovementMode() const
{
return qxt_d().movement;
}
void QxtSpanSlider::setHandleMovementMode(QxtSpanSlider::HandleMovementMode mode)
{
qxt_d().movement = mode;
}
/*!
\property QxtSpanSlider::lowerValue
\brief the lower value of the span
*/
int QxtSpanSlider::lowerValue() const
{
return qMin(qxt_d().lower, qxt_d().upper);
}
void QxtSpanSlider::setLowerValue(int lower)
{
setSpan(lower, qxt_d().upper);
}
/*!
\property QxtSpanSlider::upperValue
\brief the upper value of the span
*/
int QxtSpanSlider::upperValue() const
{
return qMax(qxt_d().lower, qxt_d().upper);
}
void QxtSpanSlider::setUpperValue(int upper)
{
setSpan(qxt_d().lower, upper);
}
/*!
Sets the span from \a lower to \a upper.
*/
void QxtSpanSlider::setSpan(int lower, int upper)
{
const int low = qBound(minimum(), qMin(lower, upper), maximum());
const int upp = qBound(minimum(), qMax(lower, upper), maximum());
if (low != qxt_d().lower || upp != qxt_d().upper)
{
if (low != qxt_d().lower)
{
qxt_d().lower = low;
qxt_d().lowerPos = low;
emit lowerValueChanged(low);
}
if (upp != qxt_d().upper)
{
qxt_d().upper = upp;
qxt_d().upperPos = upp;
emit upperValueChanged(upp);
}
emit spanChanged(qxt_d().lower, qxt_d().upper);
update();
}
}
/*!
\property QxtSpanSlider::lowerPosition
\brief the lower position of the span
*/
int QxtSpanSlider::lowerPosition() const
{
return qxt_d().lowerPos;
}
void QxtSpanSlider::setLowerPosition(int lower)
{
if (qxt_d().lowerPos != lower)
{
qxt_d().lowerPos = lower;
if (!hasTracking())
update();
if (isSliderDown())
emit lowerPositionChanged(lower);
if (hasTracking() && !qxt_d().blockTracking)
{
bool main = (qxt_d().mainControl == QxtSpanSlider::LowerHandle);
qxt_d().triggerAction(SliderMove, main);
}
}
}
/*!
\property QxtSpanSlider::upperPosition
\brief the upper position of the span
*/
int QxtSpanSlider::upperPosition() const
{
return qxt_d().upperPos;
}
void QxtSpanSlider::setUpperPosition(int upper)
{
if (qxt_d().upperPos != upper)
{
qxt_d().upperPos = upper;
if (!hasTracking())
update();
if (isSliderDown())
emit upperPositionChanged(upper);
if (hasTracking() && !qxt_d().blockTracking)
{
bool main = (qxt_d().mainControl == QxtSpanSlider::UpperHandle);
qxt_d().triggerAction(SliderMove, main);
}
}
}
/*!
\reimp
*/
void QxtSpanSlider::keyPressEvent(QKeyEvent* event)
{
QSlider::keyPressEvent(event);
bool main = true;
SliderAction action = SliderNoAction;
switch (event->key())
{
case Qt::Key_Left:
main = (orientation() == Qt::Horizontal);
action = !invertedAppearance() ? SliderSingleStepSub : SliderSingleStepAdd;
break;
case Qt::Key_Right:
main = (orientation() == Qt::Horizontal);
action = !invertedAppearance() ? SliderSingleStepAdd : SliderSingleStepSub;
break;
case Qt::Key_Up:
main = (orientation() == Qt::Vertical);
action = invertedControls() ? SliderSingleStepSub : SliderSingleStepAdd;
break;
case Qt::Key_Down:
main = (orientation() == Qt::Vertical);
action = invertedControls() ? SliderSingleStepAdd : SliderSingleStepSub;
break;
case Qt::Key_Home:
main = (qxt_d().mainControl == QxtSpanSlider::LowerHandle);
action = SliderToMinimum;
break;
case Qt::Key_End:
main = (qxt_d().mainControl == QxtSpanSlider::UpperHandle);
action = SliderToMaximum;
break;
default:
event->ignore();
break;
}
if (action)
qxt_d().triggerAction(action, main);
}
/*!
\reimp
*/
void QxtSpanSlider::mousePressEvent(QMouseEvent* event)
{
if (minimum() == maximum() || (event->buttons() ^ event->button()))
{
event->ignore();
return;
}
qxt_d().handleMousePress(event->pos(), qxt_d().upperPressed, qxt_d().upper, QxtSpanSlider::UpperHandle);
if (qxt_d().upperPressed != QStyle::SC_SliderHandle)
qxt_d().handleMousePress(event->pos(), qxt_d().lowerPressed, qxt_d().lower, QxtSpanSlider::LowerHandle);
qxt_d().firstMovement = true;
event->accept();
}
/*!
\reimp
*/
void QxtSpanSlider::mouseMoveEvent(QMouseEvent* event)
{
if (qxt_d().lowerPressed != QStyle::SC_SliderHandle && qxt_d().upperPressed != QStyle::SC_SliderHandle)
{
event->ignore();
return;
}
QStyleOptionSlider opt;
qxt_d().initStyleOption(&opt);
const int m = style()->pixelMetric(QStyle::PM_MaximumDragDistance, &opt, this);
int newPosition = qxt_d().pixelPosToRangeValue(qxt_d().pick(event->pos()) - qxt_d().offset);
if (m >= 0)
{
const QRect r = rect().adjusted(-m, -m, m, m);
if (!r.contains(event->pos()))
{
newPosition = qxt_d().position;
}
}
// pick the preferred handle on the first movement
if (qxt_d().firstMovement)
{
if (qxt_d().lower == qxt_d().upper)
{
if (newPosition < lowerValue())
{
qxt_d().swapControls();
qxt_d().firstMovement = false;
}
}
else
{
qxt_d().firstMovement = false;
}
}
if (qxt_d().lowerPressed == QStyle::SC_SliderHandle)
{
if (qxt_d().movement == NoCrossing)
newPosition = qMin(newPosition, upperValue());
else if (qxt_d().movement == NoOverlapping)
newPosition = qMin(newPosition, upperValue() - 1);
if (qxt_d().movement == FreeMovement && newPosition > qxt_d().upper)
{
qxt_d().swapControls();
setUpperPosition(newPosition);
}
else
{
setLowerPosition(newPosition);
}
}
else if (qxt_d().upperPressed == QStyle::SC_SliderHandle)
{
if (qxt_d().movement == NoCrossing)
newPosition = qMax(newPosition, lowerValue());
else if (qxt_d().movement == NoOverlapping)
newPosition = qMax(newPosition, lowerValue() + 1);
if (qxt_d().movement == FreeMovement && newPosition < qxt_d().lower)
{
qxt_d().swapControls();
setLowerPosition(newPosition);
}
else
{
setUpperPosition(newPosition);
}
}
event->accept();
}
/*!
\reimp
*/
void QxtSpanSlider::mouseReleaseEvent(QMouseEvent* event)
{
QSlider::mouseReleaseEvent(event);
setSliderDown(false);
qxt_d().lowerPressed = QStyle::SC_None;
qxt_d().upperPressed = QStyle::SC_None;
update();
}
/*!
\reimp
*/
void QxtSpanSlider::paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
QStylePainter painter(this);
// ticks
QStyleOptionSlider opt;
qxt_d().initStyleOption(&opt);
opt.subControls = QStyle::SC_SliderTickmarks;
painter.drawComplexControl(QStyle::CC_Slider, opt);
// groove
opt.sliderValue = 0;
opt.sliderPosition = 0;
opt.subControls = QStyle::SC_SliderGroove;
painter.drawComplexControl(QStyle::CC_Slider, opt);
// handle rects
opt.sliderPosition = qxt_d().lowerPos;
const QRect lr = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
const int lrv = qxt_d().pick(lr.center());
opt.sliderPosition = qxt_d().upperPos;
const QRect ur = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
const int urv = qxt_d().pick(ur.center());
// span
const int minv = qMin(lrv, urv);
const int maxv = qMax(lrv, urv);
const QPoint c = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this).center();
QRect spanRect;
if (orientation() == Qt::Horizontal)
spanRect = QRect(QPoint(minv, c.y() - 2), QPoint(maxv, c.y() + 1));
else
spanRect = QRect(QPoint(c.x() - 2, minv), QPoint(c.x() + 1, maxv));
qxt_d().drawSpan(&painter, spanRect);
// handles
switch (qxt_d().lastPressed)
{
case QxtSpanSlider::LowerHandle:
qxt_d().drawHandle(&painter, QxtSpanSlider::UpperHandle);
qxt_d().drawHandle(&painter, QxtSpanSlider::LowerHandle);
break;
case QxtSpanSlider::UpperHandle:
default:
qxt_d().drawHandle(&painter, QxtSpanSlider::LowerHandle);
qxt_d().drawHandle(&painter, QxtSpanSlider::UpperHandle);
break;
}
}

99
qxt/src/qxtspanslider.h Normal file
View File

@@ -0,0 +1,99 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtGui module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTSPANSLIDER_H
#define QXTSPANSLIDER_H
#include <QSlider>
#include "qxtnamespace.h"
#include "qxtglobal.h"
class QxtSpanSliderPrivate;
class QXT_GUI_EXPORT QxtSpanSlider : public QSlider
{
Q_OBJECT
QXT_DECLARE_PRIVATE(QxtSpanSlider)
Q_PROPERTY(int lowerValue READ lowerValue WRITE setLowerValue)
Q_PROPERTY(int upperValue READ upperValue WRITE setUpperValue)
Q_PROPERTY(int lowerPosition READ lowerPosition WRITE setLowerPosition)
Q_PROPERTY(int upperPosition READ upperPosition WRITE setUpperPosition)
Q_PROPERTY(HandleMovementMode handleMovementMode READ handleMovementMode WRITE setHandleMovementMode)
Q_ENUMS(HandleMovementMode)
public:
explicit QxtSpanSlider(QWidget* parent = 0);
explicit QxtSpanSlider(Qt::Orientation orientation, QWidget* parent = 0);
virtual ~QxtSpanSlider();
enum HandleMovementMode
{
FreeMovement,
NoCrossing,
NoOverlapping
};
enum SpanHandle
{
NoHandle,
LowerHandle,
UpperHandle
};
HandleMovementMode handleMovementMode() const;
void setHandleMovementMode(HandleMovementMode mode);
int lowerValue() const;
int upperValue() const;
int lowerPosition() const;
int upperPosition() const;
public Q_SLOTS:
void setLowerValue(int lower);
void setUpperValue(int upper);
void setSpan(int lower, int upper);
void setLowerPosition(int lower);
void setUpperPosition(int upper);
Q_SIGNALS:
void spanChanged(int lower, int upper);
void lowerValueChanged(int lower);
void upperValueChanged(int upper);
void lowerPositionChanged(int lower);
void upperPositionChanged(int upper);
void sliderPressed(SpanHandle handle);
protected:
virtual void keyPressEvent(QKeyEvent* event);
virtual void mousePressEvent(QMouseEvent* event);
virtual void mouseMoveEvent(QMouseEvent* event);
virtual void mouseReleaseEvent(QMouseEvent* event);
virtual void paintEvent(QPaintEvent* event);
};
#endif // QXTSPANSLIDER_H

75
qxt/src/qxtspanslider_p.h Normal file
View File

@@ -0,0 +1,75 @@
/****************************************************************************
**
** Copyright (C) Qxt Foundation. Some rights reserved.
**
** This file is part of the QxtGui module of the Qxt library.
**
** This library is free software; you can redistribute it and/or modify it
** under the terms of the Common Public License, version 1.0, as published
** by IBM, and/or under the terms of the GNU Lesser General Public License,
** version 2.1, as published by the Free Software Foundation.
**
** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
** FITNESS FOR A PARTICULAR PURPOSE.
**
** You should have received a copy of the CPL and the LGPL along with this
** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
** included with the source distribution for more information.
** If you did not receive a copy of the licenses, contact the Qxt Foundation.
**
** <http://libqxt.org> <foundation@libqxt.org>
**
****************************************************************************/
#ifndef QXTSPANSLIDER_P_H
#define QXTSPANSLIDER_P_H
#include <QStyle>
#include <QObject>
#include "qxtspanslider.h"
QT_FORWARD_DECLARE_CLASS(QStylePainter)
QT_FORWARD_DECLARE_CLASS(QStyleOptionSlider)
class QxtSpanSliderPrivate : public QObject, public QxtPrivate<QxtSpanSlider>
{
Q_OBJECT
public:
QXT_DECLARE_PUBLIC(QxtSpanSlider)
QxtSpanSliderPrivate();
void initStyleOption(QStyleOptionSlider* option, QxtSpanSlider::SpanHandle handle = QxtSpanSlider::UpperHandle) const;
int pick(const QPoint& pt) const
{
return qxt_p().orientation() == Qt::Horizontal ? pt.x() : pt.y();
}
int pixelPosToRangeValue(int pos) const;
void handleMousePress(const QPoint& pos, QStyle::SubControl& control, int value, QxtSpanSlider::SpanHandle handle);
void drawHandle(QStylePainter* painter, QxtSpanSlider::SpanHandle handle) const;
void setupPainter(QPainter* painter, Qt::Orientation orientation, qreal x1, qreal y1, qreal x2, qreal y2) const;
void drawSpan(QStylePainter* painter, const QRect& rect) const;
void triggerAction(QAbstractSlider::SliderAction action, bool main);
void swapControls();
int lower;
int upper;
int lowerPos;
int upperPos;
int offset;
int position;
QxtSpanSlider::SpanHandle lastPressed;
QxtSpanSlider::SpanHandle mainControl;
QStyle::SubControl lowerPressed;
QStyle::SubControl upperPressed;
QxtSpanSlider::HandleMovementMode movement;
bool firstMovement;
bool blockTracking;
public Q_SLOTS:
void updateRange(int min, int max);
void movePressedHandle();
};
#endif // QXTSPANSLIDER_P_H

5
src/.gitignore vendored
View File

@@ -17,6 +17,11 @@ profile
moc_*
qrc_application.cpp
# ignore lex/yacc generated files
*_lex.cpp
*_yacc.cpp
*_yacc.h
# ignore other object files
*.o

View File

@@ -21,7 +21,7 @@
#include "QuarqdClient.h"
#include "RealtimeData.h"
ANTplusController::ANTplusController(RealtimeWindow *parent, DeviceConfiguration *dc) : RealtimeController(parent)
ANTplusController::ANTplusController(RealtimeWindow *parent, DeviceConfiguration *dc) : RealtimeController(parent, dc)
{
myANTplus = new QuarqdClient (parent, dc);
}
@@ -82,10 +82,12 @@ ANTplusController::getRealtimeData(RealtimeData &rtData)
msgBox.setText("Cannot Connect to Quarqd");
msgBox.setIcon(QMessageBox::Critical);
msgBox.exec();
parent->Stop();
parent->Stop(1);
return;
}
// get latest telemetry
rtData = myANTplus->getRealtimeData();
processRealtimeData(rtData);
}
void ANTplusController::pushRealtimeData(RealtimeData &) { } // update realtime data with current values

View File

@@ -17,6 +17,7 @@
*/
#include "RideMetric.h"
#include <QApplication>
// This metric computes aerobic decoupling percentage as described
// by Joe Friel:
@@ -36,18 +37,27 @@
// in heart rate to power ratio as described by Friel.
class AerobicDecoupling : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(AerobicDecoupling)
double percent;
public:
AerobicDecoupling() : percent(0.0) {}
QString symbol() const { return "aerobic_decoupling"; }
QString name() const { return QObject::tr("Aerobic Decoupling"); }
QString units(bool) const { return "%"; }
int precision() const { return 2; }
double value(bool) const { return percent; }
void compute(const RideFile *ride, const Zones *, int,
AerobicDecoupling() : percent(0.0)
{
setSymbol("aerobic_decoupling");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Aerobic Decoupling");
}
void initialize() {
#endif
setName(tr("Aerobic Decoupling"));
setType(RideMetric::Average);
setMetricUnits(tr("%"));
setImperialUnits(tr("%"));
setPrecision(2);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
double firstHalfPower = 0.0, secondHalfPower = 0.0;
double firstHalfHR = 0.0, secondHalfHR = 0.0;
@@ -55,6 +65,7 @@ class AerobicDecoupling : public RideMetric {
int count = 0;
int firstHalfCount = 0;
int secondHalfCount = 0;
percent = 0;
foreach(const RideFilePoint *point, ride->dataPoints()) {
if (count++ < halfway) {
if (point->hr > 0) {
@@ -71,7 +82,7 @@ class AerobicDecoupling : public RideMetric {
}
}
}
if ((firstHalfCount > 0) && (secondHalfCount > 0)) {
if ((firstHalfPower > 0) && (secondHalfPower > 0)) {
firstHalfPower /= firstHalfCount;
secondHalfPower /= secondHalfCount;
firstHalfHR /= firstHalfCount;
@@ -80,6 +91,7 @@ class AerobicDecoupling : public RideMetric {
double secondHalfRatio = secondHalfHR / secondHalfPower;
percent = 100.0 * (secondHalfRatio - firstHalfRatio) / firstHalfRatio;
}
setValue(percent);
}
RideMetric *clone() const { return new AerobicDecoupling(*this); }

752
src/Aerolab.cpp Normal file
View File

@@ -0,0 +1,752 @@
/*
* Copyright (c) 2009 Andy M. Froncioni (me@andyfroncioni.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 "Aerolab.h"
#include "AerolabWindow.h"
#include "MainWindow.h"
#include "RideFile.h"
#include "RideItem.h"
#include "Settings.h"
#include "Units.h"
#include "Colors.h"
#include <math.h>
#include <assert.h>
#include <qwt_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_marker.h>
#include <qwt_symbol.h>
#include <set>
#include <QDebug>
#define PI M_PI
static inline double
max(double a, double b) { if (a > b) return a; else return b; }
static inline double
min(double a, double b) { if (a < b) return a; else return b; }
/*----------------------------------------------------------------------
* Interval plotting
*--------------------------------------------------------------------*/
class IntervalAerolabData : public QwtData
{
public:
Aerolab *aerolab;
MainWindow *mainWindow;
IntervalAerolabData
(
Aerolab *aerolab,
MainWindow *mainWindow
) : aerolab( aerolab ), mainWindow( mainWindow ) { }
double x( size_t ) const;
double y( size_t ) const;
size_t size() const;
virtual QwtData *copy() const;
void init();
IntervalItem *intervalNum( int ) const;
int intervalCount() const;
};
/*
* HELPER FUNCTIONS:
* intervalNum - returns a pointer to the nth selected interval
* intervalCount - returns the number of highlighted intervals
*/
// ------------------------------------------------------------------------------------------------------------
// note this is operating on the children of allIntervals and not the
// intervalWidget (QTreeWidget) -- this is why we do not use the
// selectedItems() member. N starts a one not zero.
// ------------------------------------------------------------------------------------------------------------
IntervalItem *IntervalAerolabData::intervalNum
(
int number
) const
{
int highlighted = 0;
const QTreeWidgetItem *allIntervals = mainWindow->allIntervalItems();
for ( int ii = 0; ii < allIntervals->childCount(); ii++)
{
IntervalItem *current = (IntervalItem *) allIntervals->child( ii );
if ( current == NULL)
{
return NULL;
}
if ( current->isSelected() == true )
{
++highlighted;
}
if ( highlighted == number )
{
return current;
}
}
return NULL;
}
// ------------------------------------------------------------------------------------------------------------
// how many intervals selected?
// ------------------------------------------------------------------------------------------------------------
int IntervalAerolabData::intervalCount() const
{
int highlighted = 0;
if ( mainWindow->allIntervalItems() != NULL )
{
const QTreeWidgetItem *allIntervals = mainWindow->allIntervalItems();
for ( int ii = 0; ii < allIntervals->childCount(); ii++)
{
IntervalItem *current = (IntervalItem *) allIntervals->child( ii );
if ( current != NULL )
{
if ( current->isSelected() == true )
{
++highlighted;
}
}
}
}
return highlighted;
}
/*
* INTERVAL HIGHLIGHTING CURVE
* IntervalAerolabData - implements the qwtdata interface where
* x,y return point co-ordinates and
* size returns the number of points
*/
// The interval curve data is derived from the intervals that have
// been selected in the MainWindow leftlayout for each selected
// interval we return 4 data points; bottomleft, topleft, topright
// and bottom right.
//
// the points correspond to:
// bottom left = interval start, 0 watts
// top left = interval start, maxwatts
// top right = interval stop, maxwatts
// bottom right = interval stop, 0 watts
//
double IntervalAerolabData::x
(
size_t number
) const
{
// for each interval there are four points, which interval is this for?
// interval numbers start at 1 not ZERO in the utility functions
double result = 0;
int interval_no = number ? 1 + number / 4 : 1;
// get the interval
IntervalItem *current = intervalNum( interval_no );
if ( current != NULL )
{
double multiplier = aerolab->useMetricUnits ? 1 : MILES_PER_KM;
// which point are we returning?
//qDebug() << "number = " << number << endl;
switch ( number % 4 )
{
case 0 : result = aerolab->bydist ? multiplier * current->startKM : current->start/60; // bottom left
break;
case 1 : result = aerolab->bydist ? multiplier * current->startKM : current->start/60; // top left
break;
case 2 : result = aerolab->bydist ? multiplier * current->stopKM : current->stop/60; // bottom right
break;
case 3 : result = aerolab->bydist ? multiplier * current->stopKM : current->stop/60; // top right
break;
}
}
return result;
}
double IntervalAerolabData::y
(
size_t number
) const
{
// which point are we returning?
double result = 0;
switch ( number % 4 )
{
case 0 : result = -5000; // bottom left
break;
case 1 : result = 5000; // top left - set to out of bound value
break;
case 2 : result = 5000; // top right - set to out of bound value
break;
case 3 : result = -5000; // bottom right
break;
}
return result;
}
size_t IntervalAerolabData::size() const
{
return intervalCount() * 4;
}
QwtData *IntervalAerolabData::copy() const
{
return new IntervalAerolabData( aerolab, mainWindow );
}
//**********************************************
//** END IntervalAerolabData **
//**********************************************
Aerolab::Aerolab(
AerolabWindow *parent,
MainWindow *mainWindow
):
QwtPlot(parent),
parent(parent),
unit(0),
rideItem(NULL),
smooth(1), bydist(true), autoEoffset(true) {
crr = 0.005;
cda = 0.500;
totalMass = 85.0;
rho = 1.236;
eta = 1.0;
eoffset = 0.0;
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
unit = settings->value(GC_UNIT);
useMetricUnits = true;
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setXTitle();
setAxisTitle(yLeft, tr("Elevation (m)"));
setAxisScale(yLeft, -300, 300);
setAxisTitle(xBottom, tr("Distance (km)"));
setAxisScale(xBottom, 0, 60);
veCurve = new QwtPlotCurve(tr("V-Elevation"));
altCurve = new QwtPlotCurve(tr("Elevation"));
// get rid of nasty blank space on right of the plot
veCurve->setYAxis( yLeft );
altCurve->setYAxis( yLeft );
intervalHighlighterCurve = new QwtPlotCurve();
intervalHighlighterCurve->setBaseline(-5000);
intervalHighlighterCurve->setYAxis( yLeft );
intervalHighlighterCurve->setData( IntervalAerolabData( this, mainWindow ) );
intervalHighlighterCurve->attach( this );
this->legend()->remove( intervalHighlighterCurve ); // don't show in legend
grid = new QwtPlotGrid();
grid->enableX(false);
grid->attach(this);
configChanged();
}
void
Aerolab::configChanged()
{
// set colors
setCanvasBackground(GColor(CPLOTBACKGROUND));
QPen vePen = QPen(GColor(CAEROVE));
vePen.setWidth(1);
veCurve->setPen(vePen);
QPen altPen = QPen(GColor(CAEROEL));
altPen.setWidth(1);
altCurve->setPen(altPen);
QPen gridPen(GColor(CPLOTGRID));
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
QPen ihlPen = QPen( GColor( CINTERVALHIGHLIGHTER ) );
ihlPen.setWidth(1);
intervalHighlighterCurve->setPen( ihlPen );
QColor ihlbrush = QColor(GColor(CINTERVALHIGHLIGHTER));
ihlbrush.setAlpha(40);
intervalHighlighterCurve->setBrush(ihlbrush); // fill below the line
this->legend()->remove( intervalHighlighterCurve ); // don't show in legend
}
void
Aerolab::setData(RideItem *_rideItem, bool new_zoom) {
// HARD-CODED DATA: p1->kph
double vfactor = 3.600;
double m = totalMass;
double small_number = 0.00001;
rideItem = _rideItem;
RideFile *ride = rideItem->ride();
veArray.clear();
altArray.clear();
distanceArray.clear();
timeArray.clear();
useMetricUnits = true;
if( ride ) {
const RideFileDataPresent *dataPresent = ride->areDataPresent();
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
if( dataPresent->watts ) {
// If watts are present, then we can fill the veArray data:
const RideFileDataPresent *dataPresent = ride->areDataPresent();
int npoints = ride->dataPoints().size();
double dt = ride->recIntSecs();
veArray.resize(dataPresent->watts ? npoints : 0);
altArray.resize(dataPresent->alt ? npoints : 0);
timeArray.resize(dataPresent->watts ? npoints : 0);
distanceArray.resize(dataPresent->watts ? npoints : 0);
// quickly erase old data
veCurve->setVisible(false);
altCurve->setVisible(false);
// detach and re-attach the ve curve:
veCurve->detach();
if (!veArray.empty()) {
veCurve->attach(this);
veCurve->setVisible(dataPresent->watts);
}
// detach and re-attach the ve curve:
bool have_recorded_alt_curve = false;
altCurve->detach();
if (!altArray.empty()) {
have_recorded_alt_curve = true;
altCurve->attach(this);
altCurve->setVisible(dataPresent->alt);
}
// Fill the virtual elevation profile with data from the ride data:
double t = 0.0;
double vlast = 0.0;
double e = 0.0;
double d = 0;
arrayLength = 0;
foreach(const RideFilePoint *p1, ride->dataPoints()) {
if ( arrayLength == 0 )
e = eoffset;
timeArray[arrayLength] = p1->secs / 60.0;
if ( have_recorded_alt_curve )
altArray[arrayLength] = (useMetricUnits
? p1->alt
: p1->alt * FEET_PER_METER);
// Unpack:
double power = max(0, p1->watts);
double v = p1->kph/vfactor;
double headwind = v;
if( dataPresent->headwind ) {
headwind = p1->headwind/vfactor;
}
double f = 0.0;
double a = 0.0;
// Use km data insteed of formula for file with a stop (gap).
//d += v * dt;
//distanceArray[arrayLength] = d/1000;
distanceArray[arrayLength] = p1->km;
if( v > small_number ) {
f = power/v;
a = ( v*v - vlast*vlast ) / ( 2.0 * dt * v );
} else {
a = ( v - vlast ) / dt;
}
f *= eta; // adjust for drivetrain efficiency if using a crank-based meter
double s = slope( f, a, m, crr, cda, rho, headwind );
double de = s * v * dt;
e += de;
t += dt;
veArray[arrayLength] = e;
vlast = v;
++arrayLength;
}
} else {
veCurve->setVisible(false);
altCurve->setVisible(false);
}
recalc(new_zoom);
adjustEoffset();
} else {
setTitle("no data");
}
}
void
Aerolab::adjustEoffset() {
if (autoEoffset && !altArray.empty()) {
double idx = axisScaleDiv( QwtPlot::xBottom )->lowerBound();
parent->eoffsetSlider->setEnabled(false);
if (bydist) {
int v = 100*(altArray.at(rideItem->ride()->distanceIndex(idx))-veArray.at(rideItem->ride()->distanceIndex(idx)));
parent->eoffsetSlider->setValue(intEoffset()+v);
}
else {
int v = 100*(altArray.at(rideItem->ride()->timeIndex(60*idx))-veArray.at(rideItem->ride()->timeIndex(60*idx)));
parent->eoffsetSlider->setValue(intEoffset()+v);
}
} else
parent->eoffsetSlider->setEnabled(true);
}
struct DataPoint {
double time, hr, watts, speed, cad, alt;
DataPoint(double t, double h, double w, double s, double c, double a) :
time(t), hr(h), watts(w), speed(s), cad(c), alt(a) {}
};
void
Aerolab::recalc( bool new_zoom ) {
if (timeArray.empty())
return;
int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]);
int totalRideDistance = (int ) ceil(distanceArray[arrayLength - 1]);
// If the ride is really long, then avoid it like the plague.
if (rideTimeSecs > 7*24*60*60) {
QwtArray<double> data;
if (!veArray.empty()){
veCurve->setData(data, data);
}
if( !altArray.empty()) {
altCurve->setData(data, data);
}
return;
}
QVector<double> &xaxis = (bydist?distanceArray:timeArray);
int startingIndex = 0;
int totalPoints = arrayLength - startingIndex;
// set curves
if (!veArray.empty())
veCurve->setData(xaxis.data() + startingIndex,
veArray.data() + startingIndex, totalPoints);
if (!altArray.empty()){
altCurve->setData(xaxis.data() + startingIndex,
altArray.data() + startingIndex, totalPoints);
}
if( new_zoom )
setAxisScale(xBottom, 0.0, (bydist?totalRideDistance:rideTimeSecs));
setYMax(new_zoom );
refreshIntervalMarkers();
replot();
}
void
Aerolab::setYMax(bool new_zoom)
{
if (veCurve->isVisible())
{
if ( useMetricUnits )
{
setAxisTitle( yLeft, "Elevation (m)" );
}
else
{
setAxisTitle( yLeft, "Elevation (')" );
}
double minY = 0.0;
double maxY = 0.0;
//************
//if (veCurve->isVisible()) {
// setAxisTitle(yLeft, tr("Elevation"));
if ( !altArray.empty() ) {
// setAxisScale(yLeft,
// min( veCurve->minYValue(), altCurve->minYValue() ) - 10,
// 10.0 + max( veCurve->maxYValue(), altCurve->maxYValue() ) );
minY = min( veCurve->minYValue(), altCurve->minYValue() ) - 10;
maxY = 10.0 + max( veCurve->maxYValue(), altCurve->maxYValue() );
} else {
//setAxisScale(yLeft,
// veCurve->minYValue() ,
// 1.05 * veCurve->maxYValue() );
if ( new_zoom )
{
minY = veCurve->minYValue();
maxY = veCurve->maxYValue();
}
else
{
minY = parent->getCanvasTop();
maxY = parent->getCanvasBottom();
}
//adjust eooffset
// TODO
}
setAxisScale( yLeft, minY, maxY );
setAxisLabelRotation(yLeft,270);
setAxisLabelAlignment(yLeft,Qt::AlignVCenter);
}
enableAxis(yLeft, veCurve->isVisible());
}
void
Aerolab::setXTitle() {
if (bydist)
setAxisTitle(xBottom, tr("Distance ")+QString(unit.toString() == "Metric"?"(km)":"(miles)"));
else
setAxisTitle(xBottom, tr("Time (minutes)"));
}
void
Aerolab::setAutoEoffset(int value)
{
autoEoffset = value;
adjustEoffset();
}
void
Aerolab::setByDistance(int value) {
bydist = value;
setXTitle();
recalc(true);
}
double
Aerolab::slope(
double f,
double a,
double m,
double crr,
double cda,
double rho,
double v
) {
double g = 9.80665;
// Small angle version of slope calculation:
double s = f/(m*g) - crr - cda*rho*v*v/(2.0*m*g) - a/g;
return s;
}
// At slider 1000, we want to get max Crr=0.1000
// At slider 1 , we want to get min Crr=0.0001
void
Aerolab::setIntCrr(
int value
) {
crr = (double) value / 1000000.0;
recalc(false);
}
// At slider 1000, we want to get max CdA=1.000
// At slider 1 , we want to get min CdA=0.001
void
Aerolab::setIntCda(
int value
) {
cda = (double) value / 10000.0;
recalc(false);
}
// At slider 1000, we want to get max CdA=1.000
// At slider 1 , we want to get min CdA=0.001
void
Aerolab::setIntTotalMass(
int value
) {
totalMass = (double) value / 100.0;
recalc(false);
}
// At slider 1000, we want to get max CdA=1.000
// At slider 1 , we want to get min CdA=0.001
void
Aerolab::setIntRho(
int value
) {
rho = (double) value / 10000.0;
recalc(false);
}
// At slider 1000, we want to get max CdA=1.000
// At slider 1 , we want to get min CdA=0.001
void
Aerolab::setIntEta(
int value
) {
eta = (double) value / 10000.0;
recalc(false);
}
// At slider 1000, we want to get max CdA=1.000
// At slider 1 , we want to get min CdA=0.001
void
Aerolab::setIntEoffset(
int value
) {
eoffset = (double) value / 100.0;
recalc(false);
}
void Aerolab::pointHover (QwtPlotCurve *curve, int index)
{
if ( index >= 0 && curve != intervalHighlighterCurve )
{
double x_value = curve->x( index );
double y_value = curve->y( index );
// output the tooltip
QString text = QString( "%1 %2 %3 %4 %5" )
. arg( this->axisTitle( curve->xAxis() ).text() )
. arg( x_value, 0, 'f', 3 )
. arg( "\n" )
. arg( this->axisTitle( curve->yAxis() ).text() )
. arg( y_value, 0, 'f', 3 );
// set that text up
tooltip->setText( text );
}
else
{
// no point
tooltip->setText( "" );
}
}
void Aerolab::refreshIntervalMarkers()
{
foreach( QwtPlotMarker *mrk, d_mrk )
{
mrk->detach();
delete mrk;
}
d_mrk.clear();
QRegExp wkoAuto("^(Peak *[0-9]*(s|min)|Entire workout|Find #[0-9]*) *\\([^)]*\\)$");
if ( rideItem->ride() )
{
foreach(const RideFileInterval &interval, rideItem->ride()->intervals()) {
// skip WKO autogenerated peak intervals
if (wkoAuto.exactMatch(interval.name))
continue;
QwtPlotMarker *mrk = new QwtPlotMarker;
d_mrk.append(mrk);
mrk->attach(this);
mrk->setLineStyle(QwtPlotMarker::VLine);
mrk->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
mrk->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
QwtText text(interval.name);
text.setFont(QFont("Helvetica", 10, QFont::Bold));
text.setColor(GColor(CPLOTMARKER));
if (!bydist)
mrk->setValue(interval.start / 60.0, 0.0);
else
mrk->setValue((useMetricUnits ? 1 : MILES_PER_KM) *
rideItem->ride()->timeToDistance(interval.start), 0.0);
mrk->setLabel(text);
}
}
}

139
src/Aerolab.h Normal file
View File

@@ -0,0 +1,139 @@
/*
* Copyright (c) 2009 Andy M. Froncioni (me@andyfroncioni.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_Aerolab_h
#define _GC_Aerolab_h 1
#include <qwt_plot.h>
#include <qwt_data.h>
#include <QtGui>
#include "LTMWindow.h" // for tooltip/canvaspicker
// forward references
class RideItem;
class RideFilePoint;
class QwtPlotCurve;
class QwtPlotGrid;
class QwtPlotMarker;
class AerolabWindow;
class MainWindow;
class IntervalAerolabData;
class LTMToolTip;
class LTMCanvasPicker;
class Aerolab : public QwtPlot {
Q_OBJECT
public:
Aerolab( AerolabWindow *, MainWindow * );
bool byDistance() const { return bydist; }
bool useMetricUnits; // whether metric units are used (or imperial)
void setData(RideItem *_rideItem, bool new_zoom);
void refreshIntervalMarkers();
private:
AerolabWindow *parent;
LTMToolTip *tooltip;
LTMCanvasPicker *_canvasPicker; // allow point selection/hover
void adjustEoffset();
public slots:
void setAutoEoffset(int value);
void setByDistance(int value);
void configChanged();
void pointHover( QwtPlotCurve *, int );
signals:
protected:
friend class ::AerolabWindow;
friend class ::IntervalAerolabData;
QVariant unit;
QwtPlotGrid *grid;
QVector<QwtPlotMarker*> d_mrk;
// One curve to plot in the Course Profile:
QwtPlotCurve *veCurve; // virtual elevation curve
QwtPlotCurve *altCurve; // recorded elevation curve, if available
QwtPlotCurve *intervalHighlighterCurve; // highlight selected intervals on the Plot
RideItem *rideItem;
QVector<double> hrArray;
QVector<double> wattsArray;
QVector<double> speedArray;
QVector<double> cadArray;
// We store virtual elevation, time, altitude,and distance:
QVector<double> veArray;
QVector<double> altArray;
QVector<double> timeArray;
QVector<double> distanceArray;
int smooth;
bool bydist;
bool autoEoffset;
int arrayLength;
int iCrr;
int iCda;
double crr;
double cda;
double totalMass; // Bike + Rider mass
double rho;
double eta;
double eoffset;
double slope(double, double, double, double, double, double, double);
void recalc(bool);
void setYMax(bool);
void setXTitle();
void setIntCrr(int);
void setIntCda(int);
void setIntRho(int);
void setIntEta(int);
void setIntEoffset(int);
void setIntTotalMass(int);
double getCrr() const { return (double)crr; }
double getCda() const { return (double)cda; }
double getTotalMass() const { return (double)totalMass; }
double getRho() const { return (double)rho; }
double getEta() const { return (double)eta; }
double getEoffset() const { return (double)eoffset; }
int intCrr() const { return (int)( crr * 1000000 ); }
int intCda() const { return (int)( cda * 10000); }
int intTotalMass() const { return (int)( totalMass * 100); }
int intRho() const { return (int)( rho * 10000); }
int intEta() const { return (int)( eta * 10000); }
int intEoffset() const { return (int)( eoffset * 100); }
};
#endif // _GC_Aerolab_h

518
src/AerolabWindow.cpp Normal file
View File

@@ -0,0 +1,518 @@
/*
* Copyright (c) 2009 Andy M. Froncioni (me@andyfroncioni.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 "MainWindow.h"
#include "AerolabWindow.h"
#include "Aerolab.h"
#include "RideItem.h"
#include "Colors.h"
#include <QtGui>
#include <qwt_plot_zoomer.h>
AerolabWindow::AerolabWindow(MainWindow *mainWindow) :
QWidget(mainWindow), mainWindow(mainWindow) {
// Aerolab tab layout:
QVBoxLayout *vLayout = new QVBoxLayout;
QHBoxLayout *cLayout = new QHBoxLayout;
// Plot:
aerolab = new Aerolab(this, mainWindow);
// Left controls layout:
QVBoxLayout *leftControls = new QVBoxLayout;
QFontMetrics metrics(QApplication::font());
int labelWidth1 = metrics.width("Crr") + 10;
// Crr:
QHBoxLayout *crrLayout = new QHBoxLayout;
QLabel *crrLabel = new QLabel(tr("Crr"), this);
crrLabel->setFixedWidth(labelWidth1);
crrLineEdit = new QLineEdit();
crrLineEdit->setFixedWidth(70);
crrLineEdit->setText(QString("%1").arg(aerolab->getCrr()) );
/*crrQLCDNumber = new QLCDNumber(7);
crrQLCDNumber->setMode(QLCDNumber::Dec);
crrQLCDNumber->setSmallDecimalPoint(false);
crrQLCDNumber->setSegmentStyle(QLCDNumber::Flat);
crrQLCDNumber->display(QString("%1").arg(aerolab->getCrr()) );*/
crrSlider = new QSlider(Qt::Horizontal);
crrSlider->setTickPosition(QSlider::TicksBelow);
crrSlider->setTickInterval(1000);
crrSlider->setMinimum(1000);
crrSlider->setMaximum(10000);
crrSlider->setValue(aerolab->intCrr());
crrLayout->addWidget( crrLabel );
crrLayout->addWidget( crrLineEdit );
//crrLayout->addWidget( crrQLCDNumber );
crrLayout->addWidget( crrSlider );
// CdA:
QHBoxLayout *cdaLayout = new QHBoxLayout;
QLabel *cdaLabel = new QLabel(tr("CdA"), this);
cdaLabel->setFixedWidth(labelWidth1);
cdaLineEdit = new QLineEdit();
cdaLineEdit->setFixedWidth(70);
cdaLineEdit->setText(QString("%1").arg(aerolab->getCda()) );
/*cdaQLCDNumber = new QLCDNumber(7);
cdaQLCDNumber->setMode(QLCDNumber::Dec);
cdaQLCDNumber->setSmallDecimalPoint(false);
cdaQLCDNumber->setSegmentStyle(QLCDNumber::Flat);
cdaQLCDNumber->display(QString("%1").arg(aerolab->getCda()) );*/
cdaSlider = new QSlider(Qt::Horizontal);
cdaSlider->setTickPosition(QSlider::TicksBelow);
cdaSlider->setTickInterval(100);
cdaSlider->setMinimum(1);
cdaSlider->setMaximum(6000);
cdaSlider->setValue(aerolab->intCda());
cdaLayout->addWidget( cdaLabel );
//cdaLayout->addWidget( cdaQLCDNumber );
cdaLayout->addWidget( cdaLineEdit );
cdaLayout->addWidget( cdaSlider );
// Eta:
QHBoxLayout *etaLayout = new QHBoxLayout;
QLabel *etaLabel = new QLabel(tr("Eta"), this);
etaLabel->setFixedWidth(labelWidth1);
etaLineEdit = new QLineEdit();
etaLineEdit->setFixedWidth(70);
etaLineEdit->setText(QString("%1").arg(aerolab->getEta()) );
/*etaQLCDNumber = new QLCDNumber(7);
etaQLCDNumber->setMode(QLCDNumber::Dec);
etaQLCDNumber->setSmallDecimalPoint(false);
etaQLCDNumber->setSegmentStyle(QLCDNumber::Flat);
etaQLCDNumber->display(QString("%1").arg(aerolab->getEta()) );*/
etaSlider = new QSlider(Qt::Horizontal);
etaSlider->setTickPosition(QSlider::TicksBelow);
etaSlider->setTickInterval(1000);
etaSlider->setMinimum(8000);
etaSlider->setMaximum(12000);
etaSlider->setValue(aerolab->intEta());
etaLayout->addWidget( etaLabel );
etaLayout->addWidget( etaLineEdit );
//etaLayout->addWidget( etaQLCDNumber );
etaLayout->addWidget( etaSlider );
// Add to leftControls:
leftControls->addLayout( crrLayout );
leftControls->addLayout( cdaLayout );
leftControls->addLayout( etaLayout );
// Right controls layout:
QVBoxLayout *rightControls = new QVBoxLayout;
int labelWidth2 = metrics.width("Total Mass (kg)") + 10;
// Total mass:
QHBoxLayout *mLayout = new QHBoxLayout;
QLabel *mLabel = new QLabel(tr("Total Mass (kg)"), this);
mLabel->setFixedWidth(labelWidth2);
mLineEdit = new QLineEdit();
mLineEdit->setFixedWidth(70);
mLineEdit->setText(QString("%1").arg(aerolab->getTotalMass()) );
/*mQLCDNumber = new QLCDNumber(7);
mQLCDNumber->setMode(QLCDNumber::Dec);
mQLCDNumber->setSmallDecimalPoint(false);
mQLCDNumber->setSegmentStyle(QLCDNumber::Flat);
mQLCDNumber->display(QString("%1").arg(aerolab->getTotalMass()) );*/
mSlider = new QSlider(Qt::Horizontal);
mSlider->setTickPosition(QSlider::TicksBelow);
mSlider->setTickInterval(1000);
mSlider->setMinimum(3500);
mSlider->setMaximum(15000);
mSlider->setValue(aerolab->intTotalMass());
mLayout->addWidget( mLabel );
mLayout->addWidget( mLineEdit );
//mLayout->addWidget( mQLCDNumber );
mLayout->addWidget( mSlider );
// Rho:
QHBoxLayout *rhoLayout = new QHBoxLayout;
QLabel *rhoLabel = new QLabel(tr("Rho (kg/m^3)"), this);
rhoLabel->setFixedWidth(labelWidth2);
rhoLineEdit = new QLineEdit();
rhoLineEdit->setFixedWidth(70);
rhoLineEdit->setText(QString("%1").arg(aerolab->getRho()) );
/*rhoQLCDNumber = new QLCDNumber(7);
rhoQLCDNumber->setMode(QLCDNumber::Dec);
rhoQLCDNumber->setSmallDecimalPoint(false);
rhoQLCDNumber->setSegmentStyle(QLCDNumber::Flat);
rhoQLCDNumber->display(QString("%1").arg(aerolab->getRho()) );*/
rhoSlider = new QSlider(Qt::Horizontal);
rhoSlider->setTickPosition(QSlider::TicksBelow);
rhoSlider->setTickInterval(1000);
rhoSlider->setMinimum(9000);
rhoSlider->setMaximum(14000);
rhoSlider->setValue(aerolab->intRho());
rhoLayout->addWidget( rhoLabel );
rhoLayout->addWidget( rhoLineEdit );
//rhoLayout->addWidget( rhoQLCDNumber );
rhoLayout->addWidget( rhoSlider );
// Elevation offset:
QHBoxLayout *eoffsetLayout = new QHBoxLayout;
QLabel *eoffsetLabel = new QLabel(tr("Eoffset (m)"), this);
eoffsetLabel->setFixedWidth(labelWidth2);
eoffsetLineEdit = new QLineEdit();
eoffsetLineEdit->setFixedWidth(70);
eoffsetLineEdit->setText(QString("%1").arg(aerolab->getEoffset()) );
/*eoffsetQLCDNumber = new QLCDNumber(7);
eoffsetQLCDNumber->setMode(QLCDNumber::Dec);
eoffsetQLCDNumber->setSmallDecimalPoint(false);
eoffsetQLCDNumber->setSegmentStyle(QLCDNumber::Flat);
eoffsetQLCDNumber->display(QString("%1").arg(aerolab->getEoffset()) );*/
eoffsetSlider = new QSlider(Qt::Horizontal);
eoffsetSlider->setTickPosition(QSlider::TicksBelow);
eoffsetSlider->setTickInterval(20000);
eoffsetSlider->setMinimum(-30000);
eoffsetSlider->setMaximum(250000);
eoffsetSlider->setValue(aerolab->intEoffset());
eoffsetLayout->addWidget( eoffsetLabel );
eoffsetLayout->addWidget( eoffsetLineEdit );
//eoffsetLayout->addWidget( eoffsetQLCDNumber );
eoffsetLayout->addWidget( eoffsetSlider );
QCheckBox *eoffsetAuto = new QCheckBox(tr("eoffset auto"), this);
eoffsetAuto->setCheckState(Qt::Checked);
eoffsetLayout->addWidget(eoffsetAuto);
QHBoxLayout *smoothLayout = new QHBoxLayout;
QComboBox *comboDistance = new QComboBox();
comboDistance->addItem(tr("X Axis Shows Time"));
comboDistance->addItem(tr("X Axis Shows Distance"));
comboDistance->setCurrentIndex(1);
smoothLayout->addWidget(comboDistance);
// Add to leftControls:
rightControls->addLayout( mLayout );
rightControls->addLayout( rhoLayout );
rightControls->addLayout( eoffsetLayout );
rightControls->addLayout( smoothLayout );
// Assemble controls layout:
cLayout->addLayout(leftControls);
cLayout->addLayout(rightControls);
// Zoomer:
allZoomer = new QwtPlotZoomer(aerolab->canvas());
allZoomer->setRubberBand(QwtPicker::RectRubberBand);
allZoomer->setSelectionFlags(QwtPicker::DragSelection
| QwtPicker::CornerToCorner);
allZoomer->setTrackerMode(QwtPicker::AlwaysOff);
allZoomer->setEnabled(true);
allZoomer->setMousePattern( QwtEventPattern::MouseSelect2, Qt::RightButton, Qt::ControlModifier );
allZoomer->setMousePattern( QwtEventPattern::MouseSelect3, Qt::RightButton );
// SIGNALs to SLOTs:
connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
connect(crrSlider, SIGNAL(valueChanged(int)),this, SLOT(setCrrFromSlider()));
connect(crrLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(setCrrFromText(const QString)));
connect(cdaSlider, SIGNAL(valueChanged(int)), this, SLOT(setCdaFromSlider()));
connect(cdaLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(setCdaFromText(const QString)));
connect(mSlider, SIGNAL(valueChanged(int)),this, SLOT(setTotalMassFromSlider()));
connect(mLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(setTotalMassFromText(const QString)));
connect(rhoSlider, SIGNAL(valueChanged(int)), this, SLOT(setRhoFromSlider()));
connect(rhoLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(setRhoFromText(const QString)));
connect(etaSlider, SIGNAL(valueChanged(int)), this, SLOT(setEtaFromSlider()));
connect(etaLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(setEtaFromText(const QString)));
connect(eoffsetSlider, SIGNAL(valueChanged(int)), this, SLOT(setEoffsetFromSlider()));
connect(eoffsetLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(setEoffsetFromText(const QString)));
connect(eoffsetAuto, SIGNAL(stateChanged(int)), this, SLOT(setAutoEoffset(int)));
connect(comboDistance, SIGNAL(currentIndexChanged(int)), this, SLOT(setByDistance(int)));
connect(mainWindow, SIGNAL(configChanged()), aerolab, SLOT(configChanged()));
connect(mainWindow, SIGNAL(configChanged()), this, SLOT(configChanged()));
connect(mainWindow, SIGNAL( intervalSelected() ), this, SLOT(intervalSelected()));
connect(allZoomer, SIGNAL( zoomed(const QwtDoubleRect) ), this, SLOT(zoomChanged()));
// Build the tab layout:
vLayout->addWidget(aerolab);
vLayout->addLayout(cLayout);
setLayout(vLayout);
// tooltip on hover over point
//************************************
aerolab->tooltip = new LTMToolTip( QwtPlot::xBottom,
QwtPlot::yLeft,
QwtPicker::PointSelection,
QwtPicker::VLineRubberBand,
QwtPicker::AlwaysOn,
aerolab->canvas(),
""
);
aerolab->tooltip->setSelectionFlags( QwtPicker::PointSelection | QwtPicker::RectSelection | QwtPicker::DragSelection);
aerolab->tooltip->setRubberBand( QwtPicker::VLineRubberBand );
aerolab->tooltip->setMousePattern( QwtEventPattern::MouseSelect1, Qt::LeftButton, Qt::ShiftModifier );
aerolab->tooltip->setTrackerPen( QColor( Qt::black ) );
QColor inv( Qt::white );
inv.setAlpha( 0 );
aerolab->tooltip->setRubberBandPen( inv );
aerolab->tooltip->setEnabled( true );
aerolab->_canvasPicker = new LTMCanvasPicker( aerolab );
connect( aerolab->_canvasPicker, SIGNAL( pointHover( QwtPlotCurve*, int ) ),
aerolab, SLOT ( pointHover( QwtPlotCurve*, int ) ) );
configChanged(); // pickup colors etc
}
void
AerolabWindow::zoomChanged()
{
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
void
AerolabWindow::configChanged()
{
allZoomer->setRubberBandPen(GColor(CPLOTSELECT));
}
void
AerolabWindow::rideSelected() {
if (mainWindow->activeTab() != this)
return;
RideItem *ride = mainWindow->rideItem();
if (!ride)
return;
aerolab->setData(ride, true);
allZoomer->setZoomBase();
}
void
AerolabWindow::setCrrFromText(const QString text) {
int value = 1000000 * text.toDouble();
if (aerolab->intCrr() != value) {
aerolab->setIntCrr(value);
//crrQLCDNumber->display(QString("%1").arg(aerolab->getCrr()));
crrSlider->setValue(aerolab->intCrr());
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setCrrFromSlider() {
if (aerolab->intCrr() != crrSlider->value()) {
aerolab->setIntCrr(crrSlider->value());
//crrQLCDNumber->display(QString("%1").arg(aerolab->getCrr()));
crrLineEdit->setText(QString("%1").arg(aerolab->getCrr()) );
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setCdaFromText(const QString text) {
int value = 10000 * text.toDouble();
if (aerolab->intCda() != value) {
aerolab->setIntCda(value);
//cdaQLCDNumber->display(QString("%1").arg(aerolab->getCda()));
cdaSlider->setValue(aerolab->intCda());
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setCdaFromSlider() {
if (aerolab->intCda() != cdaSlider->value()) {
aerolab->setIntCda(cdaSlider->value());
//cdaQLCDNumber->display(QString("%1").arg(aerolab->getCda()));
cdaLineEdit->setText(QString("%1").arg(aerolab->getCda()) );
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setTotalMassFromText(const QString text) {
int value = 100 * text.toDouble();
if (value == 0)
value = 1; // mass can not be zero !
if (aerolab->intTotalMass() != value) {
aerolab->setIntTotalMass(value);
//mQLCDNumber->display(QString("%1").arg(aerolab->getTotalMass()));
mSlider->setValue(aerolab->intTotalMass());
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setTotalMassFromSlider() {
if (aerolab->intTotalMass() != mSlider->value()) {
aerolab->setIntTotalMass(mSlider->value());
//mQLCDNumber->display(QString("%1").arg(aerolab->getTotalMass()));
mLineEdit->setText(QString("%1").arg(aerolab->getTotalMass()) );
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setRhoFromText(const QString text) {
int value = 10000 * text.toDouble();
if (aerolab->intRho() != value) {
aerolab->setIntRho(value);
//rhoQLCDNumber->display(QString("%1").arg(aerolab->getRho()));
rhoSlider->setValue(aerolab->intRho());
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setRhoFromSlider() {
if (aerolab->intRho() != rhoSlider->value()) {
aerolab->setIntRho(rhoSlider->value());
//rhoQLCDNumber->display(QString("%1").arg(aerolab->getRho()));
rhoLineEdit->setText(QString("%1").arg(aerolab->getRho()) );
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setEtaFromText(const QString text) {
int value = 10000 * text.toDouble();
if (aerolab->intEta() != value) {
aerolab->setIntEta(value);
//etaQLCDNumber->display(QString("%1").arg(aerolab->getEta()));
etaSlider->setValue(aerolab->intEta());
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setEtaFromSlider() {
if (aerolab->intEta() != etaSlider->value()) {
aerolab->setIntEta(etaSlider->value());
//etaQLCDNumber->display(QString("%1").arg(aerolab->getEta()));
etaLineEdit->setText(QString("%1").arg(aerolab->getEta()) );
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setEoffsetFromText(const QString text) {
int value = 100 * text.toDouble();
if (aerolab->intEoffset() != value) {
aerolab->setIntEoffset(value);
//eoffsetQLCDNumber->display(QString("%1").arg(aerolab->getEoffset()));
eoffsetSlider->setValue(aerolab->intEoffset());
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setEoffsetFromSlider() {
if (aerolab->intEoffset() != eoffsetSlider->value()) {
aerolab->setIntEoffset(eoffsetSlider->value());
//eoffsetQLCDNumber->display(QString("%1").arg(aerolab->getEoffset()));
eoffsetLineEdit->setText(QString("%1").arg(aerolab->getEoffset()) );
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
}
void
AerolabWindow::setAutoEoffset(int value)
{
aerolab->setAutoEoffset(value);
}
void
AerolabWindow::setByDistance(int value)
{
aerolab->setByDistance(value);
// refresh
RideItem *ride = mainWindow->rideItem();
aerolab->setData(ride, false);
}
void
AerolabWindow::zoomInterval(IntervalItem *which) {
QwtDoubleRect rect;
if (!aerolab->byDistance()) {
rect.setLeft(which->start/60);
rect.setRight(which->stop/60);
} else {
rect.setLeft(which->startKM);
rect.setRight(which->stopKM);
}
rect.setTop(aerolab->veCurve->maxYValue()*1.1);
rect.setBottom(aerolab->veCurve->minYValue()-10);
allZoomer->zoom(rect);
aerolab->recalc(false);
}
void AerolabWindow::intervalSelected()
{
if ( mainWindow->activeTab() != this )
{
return;
}
RideItem *ride = mainWindow->rideItem();
if ( !ride )
{
return;
}
// set the elevation data
aerolab->setData( ride, true );
}
double AerolabWindow::getCanvasTop() const
{
const QwtDoubleRect &canvasRect = allZoomer->zoomRect();
return canvasRect.top();
}
double AerolabWindow::getCanvasBottom() const
{
const QwtDoubleRect &canvasRect = allZoomer->zoomRect();
return canvasRect.bottom();
}

97
src/AerolabWindow.h Normal file
View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2009 Andy M. Froncioni (me@andyfroncioni.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_AerolabWindow_h
#define _GC_AerolabWindow_h 1
#include <QtGui>
class Aerolab;
class MainWindow;
class QCheckBox;
class QwtPlotZoomer;
class QwtPlotPicker;
class QLineEdit;
class QLCDNumber;
class RideItem;
class IntervalItem;
class AerolabWindow : public QWidget {
Q_OBJECT
public:
AerolabWindow(MainWindow *mainWindow);
void setData(RideItem *ride);
void zoomInterval(IntervalItem *); // zoom into a specified interval
double getCanvasTop() const;
double getCanvasBottom() const;
QSlider *eoffsetSlider;
public slots:
void setCrrFromSlider();
void setCrrFromText(const QString text);
void setCdaFromSlider();
void setCdaFromText(const QString text);
void setTotalMassFromSlider();
void setTotalMassFromText(const QString text);
void setRhoFromSlider();
void setRhoFromText(const QString text);
void setEtaFromSlider();
void setEtaFromText(const QString text);
void setEoffsetFromSlider();
void setEoffsetFromText(const QString text);
void setAutoEoffset(int value);
void setByDistance(int value);
void rideSelected();
void zoomChanged();
void configChanged();
void intervalSelected();
protected slots:
protected:
MainWindow *mainWindow;
Aerolab *aerolab;
QwtPlotZoomer *allZoomer;
// Bike parameter controls:
QSlider *crrSlider;
QLineEdit *crrLineEdit;
//QLCDNumber *crrQLCDNumber;
QSlider *cdaSlider;
QLineEdit *cdaLineEdit;
//QLCDNumber *cdaQLCDNumber;
QSlider *mSlider;
QLineEdit *mLineEdit;
//QLCDNumber *mQLCDNumber;
QSlider *rhoSlider;
QLineEdit *rhoLineEdit;
//QLCDNumber *rhoQLCDNumber;
QSlider *etaSlider;
QLineEdit *etaLineEdit;
//QLCDNumber *etaQLCDNumber;
QLineEdit *eoffsetLineEdit;
//QLCDNumber *eoffsetQLCDNumber;
};
#endif // _GC_AerolabWindow_h

View File

@@ -25,10 +25,12 @@
#include "Settings.h"
#include "Units.h"
#include "Zones.h"
#include "Colors.h"
#include <assert.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_layout.h>
#include <qwt_plot_marker.h>
#include <qwt_text.h>
#include <qwt_legend.h>
@@ -82,7 +84,6 @@ class AllPlotBackground: public QwtPlotItem
const Zones *zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
if (parent->shadeZones() && (zone_range >= 0)) {
QList <int> zone_lows = zones->getZoneLows(zone_range);
int num_zones = zone_lows.size();
@@ -103,6 +104,7 @@ class AllPlotBackground: public QwtPlotItem
painter->fillRect(r, shading_color);
}
}
} else {
}
}
};
@@ -152,7 +154,12 @@ class AllPlotZoneLabel: public QwtPlotItem
);
text = QwtText(zone_names[zone_number]);
text.setFont(QFont("Helvetica",24, QFont::Bold));
if (_parent->referencePlot == NULL) {
text.setFont(QFont("Helvetica",24, QFont::Bold));
} else {
text.setFont(QFont("Helvetica",12, QFont::Bold));
}
QColor text_color = zoneColor(zone_number, num_zones);
text_color.setAlpha(64);
text.setColor(text_color);
@@ -186,90 +193,127 @@ class AllPlotZoneLabel: public QwtPlotItem
static inline double
max(double a, double b) { if (a > b) return a; else return b; }
AllPlot::AllPlot(QWidget *parent, MainWindow *mainWindow):
AllPlot::AllPlot(AllPlotWindow *parent, MainWindow *mainWindow):
QwtPlot(parent),
settings(NULL),
unit(0),
rideItem(NULL),
bydist(false),
unit(0),
shade_zones(true),
showPowerState(0),
showPowerState(3),
showHrState(Qt::Checked),
showSpeedState(Qt::Checked),
showCadState(Qt::Checked),
showAltState(Qt::Checked)
showAltState(Qt::Checked),
bydist(false),
parent(parent)
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
unit = settings->value(GC_UNIT);
referencePlot = NULL;
useMetricUnits = (unit.toString() == "Metric");
// options for turning off/on shading on all plot
// will come in with a future patch, for now we
// enable zone shading by default, since this is
// the current default behaviour
if (false) shade_zones = false;
else shade_zones = true;
smooth = settings->value(GC_RIDE_PLOT_SMOOTHING).toInt();
if (smooth < 2)
smooth = 30;
if (smooth < 1) smooth = 1;
// create a background object for shading
bg = new AllPlotBackground(this);
bg->attach(this);
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setCanvasBackground(GColor(CPLOTBACKGROUND));
setXTitle();
wattsCurve = new QwtPlotCurve(tr("Power"));
QPen wattsPen = QPen(Qt::red);
wattsPen.setWidth(2);
wattsCurve->setPen(wattsPen);
hrCurve = new QwtPlotCurve(tr("Heart Rate"));
QPen hrPen = QPen(Qt::blue);
hrPen.setWidth(2);
hrCurve->setPen(hrPen);
hrCurve->setYAxis(yLeft2);
speedCurve = new QwtPlotCurve(tr("Speed"));
QPen speedPen = QPen(QColor(0, 204, 0));
speedPen.setWidth(2);
speedCurve->setPen(speedPen);
speedCurve->setYAxis(yRight);
cadCurve = new QwtPlotCurve(tr("Cadence"));
QPen cadPen = QPen(QColor(0, 204, 204));
cadPen.setWidth(2);
cadCurve->setPen(cadPen);
cadCurve->setYAxis(yLeft2);
altCurve = new QwtPlotCurve(tr("Altitude"));
// altCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen altPen(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
altCurve->setYAxis(yRight2);
intervalHighlighterCurve = new QwtPlotCurve();
QPen ihlPen = QPen(Qt::blue);
ihlPen.setWidth(2);
intervalHighlighterCurve->setPen(ihlPen);
intervalHighlighterCurve->setYAxis(yLeft);
QColor ihlbrush = QColor(Qt::blue);
ihlbrush.setAlpha(64);
intervalHighlighterCurve->setBrush(ihlbrush); // fill below the line
intervalHighlighterCurve->setData(IntervalPlotData(this, mainWindow));
intervalHighlighterCurve->attach(this);
this->legend()->remove(intervalHighlighterCurve); // don't show in legend
grid = new QwtPlotGrid();
grid->enableX(false);
QPen gridPen;
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
grid->attach(this);
zoneLabels = QList <AllPlotZoneLabel *>::QList();
// get rid of nasty blank space on right of the plot
plotLayout()->setAlignCanvasToScales(true);
configChanged(); // set colors
}
void
AllPlot::configChanged()
{
double width = 1.0;
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
useMetricUnits = (settings->value(GC_UNIT).toString() == "Metric");
// placeholder for setting antialiasing, will come
// in with a future patch. For now antialiasing is
// not enabled since it can slow down plotting on
// windows and linux platforms.
if (false) {
wattsCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
hrCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
speedCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
cadCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
altCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
intervalHighlighterCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
}
setCanvasBackground(GColor(CPLOTBACKGROUND));
QPen wattsPen = QPen(GColor(CPOWER));
wattsPen.setWidth(width);
wattsCurve->setPen(wattsPen);
QPen hrPen = QPen(GColor(CHEARTRATE));
hrPen.setWidth(width);
hrCurve->setPen(hrPen);
QPen speedPen = QPen(GColor(CSPEED));
speedPen.setWidth(width);
speedCurve->setPen(speedPen);
QPen cadPen = QPen(GColor(CCADENCE));
cadPen.setWidth(width);
cadCurve->setPen(cadPen);
QPen altPen(GColor(CALTITUDE));
altPen.setWidth(width);
altCurve->setPen(altPen);
QColor brush_color = GColor(CALTITUDEBRUSH);
brush_color.setAlpha(200);
altCurve->setBrush(brush_color); // fill below the line
QPen ihlPen = QPen(GColor(CINTERVALHIGHLIGHTER));
ihlPen.setWidth(width);
intervalHighlighterCurve->setPen(ihlPen);
QColor ihlbrush = QColor(GColor(CINTERVALHIGHLIGHTER));
ihlbrush.setAlpha(64);
intervalHighlighterCurve->setBrush(ihlbrush); // fill below the line
this->legend()->remove(intervalHighlighterCurve); // don't show in legend
QPen gridPen(GColor(CPLOTGRID));
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
}
struct DataPoint {
@@ -280,7 +324,7 @@ struct DataPoint {
bool AllPlot::shadeZones() const
{
return (shade_zones && !wattsArray.empty());
return shade_zones;
}
void AllPlot::refreshZoneLabels()
@@ -311,8 +355,13 @@ void AllPlot::refreshZoneLabels()
void
AllPlot::recalc()
{
if (referencePlot !=NULL){
return;
}
if (timeArray.empty())
return;
int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]);
if (rideTimeSecs > 7*24*60*60) {
QwtArray<double> data;
@@ -337,13 +386,13 @@ AllPlot::recalc()
QList<DataPoint> list;
QVector<double> smoothWatts(rideTimeSecs + 1);
QVector<double> smoothHr(rideTimeSecs + 1);
QVector<double> smoothSpeed(rideTimeSecs + 1);
QVector<double> smoothCad(rideTimeSecs + 1);
QVector<double> smoothTime(rideTimeSecs + 1);
QVector<double> smoothDistance(rideTimeSecs + 1);
QVector<double> smoothAltitude(rideTimeSecs + 1);
smoothWatts.resize(rideTimeSecs + 1); //(rideTimeSecs + 1);
smoothHr.resize(rideTimeSecs + 1);
smoothSpeed.resize(rideTimeSecs + 1);
smoothCad.resize(rideTimeSecs + 1);
smoothTime.resize(rideTimeSecs + 1);
smoothDistance.resize(rideTimeSecs + 1);
smoothAltitude.resize(rideTimeSecs + 1);
for (int secs = 0; ((secs < smooth)
&& (secs < rideTimeSecs)); ++secs) {
@@ -423,12 +472,11 @@ AllPlot::recalc()
if (!altArray.empty())
altCurve->setData(xaxis.data() + startingIndex, smoothAltitude.data() + startingIndex, totalPoints);
setAxisScale(xBottom, 0.0, bydist ? totalDist : smoothTime[rideTimeSecs]);
setYMax();
refreshIntervalMarkers();
refreshZoneLabels();
replot();
//replot();
}
void
@@ -450,10 +498,10 @@ AllPlot::refreshIntervalMarkers()
mrk->attach(this);
mrk->setLineStyle(QwtPlotMarker::VLine);
mrk->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
mrk->setLinePen(QPen(Qt::black, 0, Qt::DashDotLine));
mrk->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
QwtText text(interval.name);
text.setFont(QFont("Helvetica", 10, QFont::Bold));
text.setColor(Qt::black);
text.setColor(GColor(CPLOTMARKER));
if (!bydist)
mrk->setValue(interval.start / 60.0, 0.0);
else
@@ -462,16 +510,17 @@ AllPlot::refreshIntervalMarkers()
mrk->setLabel(text);
}
}
}
void
AllPlot::setYMax()
{
if (wattsCurve->isVisible()) {
setAxisTitle(yLeft, "Watts");
setAxisScale(yLeft, 0.0, 1.05 * wattsCurve->maxYValue());
setAxisTitle(yLeft, tr("Watts"));
if (referencePlot == NULL)
setAxisScale(yLeft, 0.0, 1.05 * wattsCurve->maxYValue());
else
setAxisScale(yLeft, 0.0, 1.05 * referencePlot->wattsCurve->maxYValue());
setAxisLabelRotation(yLeft,270);
setAxisLabelAlignment(yLeft,Qt::AlignVCenter);
}
@@ -479,12 +528,18 @@ AllPlot::setYMax()
double ymax = 0;
QStringList labels;
if (hrCurve->isVisible()) {
labels << "BPM";
ymax = hrCurve->maxYValue();
labels << tr("BPM");
if (referencePlot == NULL)
ymax = hrCurve->maxYValue();
else
ymax = referencePlot->hrCurve->maxYValue();
}
if (cadCurve->isVisible()) {
labels << "RPM";
ymax = qMax(ymax, cadCurve->maxYValue());
labels << tr("RPM");
if (referencePlot == NULL)
ymax = qMax(ymax, cadCurve->maxYValue());
else
ymax = qMax(ymax, referencePlot->cadCurve->maxYValue());
}
setAxisTitle(yLeft2, labels.join(" / "));
setAxisScale(yLeft2, 0.0, 1.05 * ymax);
@@ -492,15 +547,25 @@ AllPlot::setYMax()
setAxisLabelAlignment(yLeft2,Qt::AlignVCenter);
}
if (speedCurve->isVisible()) {
setAxisTitle(yRight, (useMetricUnits ? tr("KPH") : tr("MPH")));
setAxisScale(yRight, 0.0, 1.05 * speedCurve->maxYValue());
setAxisTitle(yRight, (useMetricUnits ? tr("km/h") : tr("MPH")));
if (referencePlot == NULL)
setAxisScale(yRight, 0.0, 1.05 * speedCurve->maxYValue());
else
setAxisScale(yRight, 0.0, 1.05 * referencePlot->speedCurve->maxYValue());
setAxisLabelRotation(yRight,90);
setAxisLabelAlignment(yRight,Qt::AlignVCenter);
}
if (altCurve->isVisible()) {
setAxisTitle(yRight2, useMetricUnits ? tr("Meters") : tr("Feet"));
double ymin = altCurve->minYValue();
double ymax = qMax(ymin + 100, 1.05 * altCurve->maxYValue());
double ymin,ymax;
if (referencePlot == NULL) {
ymin = altCurve->minYValue();
ymax = qMax(ymin + 100, 1.05 * altCurve->maxYValue());
} else {
ymin = referencePlot->altCurve->minYValue();
ymax = qMax(ymin + 100, 1.05 * referencePlot->altCurve->maxYValue());
}
setAxisScale(yRight2, ymin, ymax);
setAxisLabelRotation(yRight2,90);
setAxisLabelAlignment(yRight2,Qt::AlignVCenter);
@@ -523,15 +588,88 @@ AllPlot::setXTitle()
}
void
AllPlot::setData(RideItem *_rideItem)
AllPlot::setDataFromPlot(AllPlot *plot, int startidx, int stopidx)
{
if (plot == NULL) return;
referencePlot = plot;
setTitle(plot->rideItem->ride()->startTime().toString(GC_DATETIME_FORMAT));
// reference the plot for data and state
rideItem = plot->rideItem;
bydist = plot->bydist;
arrayLength = stopidx-startidx;
if (bydist) {
startidx = plot->distanceIndex(plot->distanceArray[startidx]);
stopidx = plot->distanceIndex(plot->distanceArray[(stopidx>=plot->distanceArray.size()?plot->distanceArray.size()-1:stopidx)])-1;
} else {
startidx = plot->timeIndex(plot->timeArray[startidx]/60);
stopidx = plot->timeIndex(plot->timeArray[(stopidx>=plot->timeArray.size()?plot->timeArray.size()-1:stopidx)]/60)-1;
}
// make sure indexes are still valid
if (startidx > stopidx || startidx < 0 || stopidx < 0) return;
double *smoothW = &plot->smoothWatts[startidx];
double *smoothT = &plot->smoothTime[startidx];
double *smoothHR = &plot->smoothHr[startidx];
double *smoothS = &plot->smoothSpeed[startidx];
double *smoothC = &plot->smoothCad[startidx];
double *smoothA = &plot->smoothAltitude[startidx];
double *smoothD = &plot->smoothDistance[startidx];
double *xaxis = bydist ? smoothD : smoothT;
// attach appropriate curves
if (this->legend()) this->legend()->hide();
wattsCurve->detach();
hrCurve->detach();
speedCurve->detach();
cadCurve->detach();
altCurve->detach();
wattsCurve->setData(xaxis,smoothW,stopidx-startidx);
hrCurve->setData(xaxis, smoothHR,stopidx-startidx);
speedCurve->setData(xaxis, smoothS, stopidx-startidx);
cadCurve->setData(xaxis, smoothC, stopidx-startidx);
altCurve->setData(xaxis, smoothA, stopidx-startidx);
setYMax();
setAxisMaxMajor(yLeft, 5);
setAxisMaxMajor(yLeft2, 5);
setAxisMaxMajor(yRight, 5);
setAxisMaxMajor(yRight2, 5);
setAxisScale(xBottom, xaxis[0], xaxis[stopidx-startidx-1]);
if (!plot->smoothAltitude.empty()) altCurve->attach(this);
if (!plot->smoothWatts.empty()) wattsCurve->attach(this);
if (!plot->smoothHr.empty()) hrCurve->attach(this);
if (!plot->smoothSpeed.empty()) speedCurve->attach(this);
if (!plot->smoothCad.empty()) cadCurve->attach(this);
refreshIntervalMarkers();
refreshZoneLabels();
if (this->legend()) this->legend()->show();
//replot();
}
void
AllPlot::setDataFromRide(RideItem *_rideItem)
{
rideItem = _rideItem;
if (_rideItem == NULL) return;
wattsArray.clear();
RideFile *ride = rideItem->ride();
if (ride && ride->deviceType() != QString("Manual CSV")) {
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
const RideFileDataPresent *dataPresent = ride->areDataPresent();
int npoints = ride->dataPoints().size();
@@ -549,11 +687,11 @@ AllPlot::setData(RideItem *_rideItem)
speedCurve->detach();
cadCurve->detach();
altCurve->detach();
if (!altArray.empty()) altCurve->attach(this);
if (!wattsArray.empty()) wattsCurve->attach(this);
if (!hrArray.empty()) hrCurve->attach(this);
if (!speedArray.empty()) speedCurve->attach(this);
if (!cadArray.empty()) cadCurve->attach(this);
if (!altArray.empty()) altCurve->attach(this);
wattsCurve->setVisible(dataPresent->watts && showPowerState < 2);
hrCurve->setVisible(dataPresent->hr && showHrState == Qt::Checked);
@@ -563,7 +701,21 @@ AllPlot::setData(RideItem *_rideItem)
arrayLength = 0;
foreach (const RideFilePoint *point, ride->dataPoints()) {
timeArray[arrayLength] = point->secs;
// we round the time to nearest 100th of a second
// before adding to the array, to avoid situation
// where 'high precision' time slice is an artefact
// of double precision or slight timing anomalies
// e.g. where realtime gives timestamps like
// 940.002 followed by 940.998 and were previouslt
// both rounded to 940s
//
// NOTE: this rounding mechanism is identical to that
// used by the Ride Editor.
double secs = floor(point->secs);
double msecs = round((point->secs - secs) * 100) * 10;
timeArray[arrayLength] = secs + msecs/1000;
if (!wattsArray.empty())
wattsArray[arrayLength] = max(0, point->watts);
if (!hrArray.empty())
@@ -585,11 +737,11 @@ AllPlot::setData(RideItem *_rideItem)
: point->km * MILES_PER_KM));
++arrayLength;
}
recalc();
}
else {
setTitle("no data");
wattsCurve->detach();
hrCurve->detach();
speedCurve->detach();
@@ -604,11 +756,17 @@ AllPlot::setData(RideItem *_rideItem)
void
AllPlot::showPower(int id)
{
if (showPowerState == id) return;
showPowerState = id;
wattsCurve->setVisible(id < 2);
shade_zones = (id == 0);
setYMax();
recalc();
if (shade_zones) {
bg->attach(this);
refreshZoneLabels();
} else
bg->detach();
}
void
@@ -676,6 +834,35 @@ AllPlot::setByDistance(int id)
recalc();
}
struct ComparePoints {
bool operator()(const double p1, const double p2) {
return p1 < p2;
}
};
int
AllPlot::timeIndex(double min) const
{
// return index offset for specified time
QVector<double>::const_iterator i = std::lower_bound(
smoothTime.begin(), smoothTime.end(), min, ComparePoints());
if (i == smoothTime.end())
return smoothTime.size();
return i - smoothTime.begin();
}
int
AllPlot::distanceIndex(double km) const
{
// return index offset for specified distance in km
QVector<double>::const_iterator i = std::lower_bound(
smoothDistance.begin(), smoothDistance.end(), km, ComparePoints());
if (i == smoothDistance.end())
return smoothDistance.size();
return i - smoothDistance.begin();
}
/*----------------------------------------------------------------------
* Interval plotting
*--------------------------------------------------------------------*/
@@ -758,10 +945,10 @@ double IntervalPlotData::x(size_t i) const
// which point are we returning?
switch (i%4) {
case 0 : return allPlot->byDistance() ? multiplier * current->startKM : current->start/60; // bottom left
case 1 : return allPlot->byDistance() ? multiplier * current->startKM : current->start/60; // top left
case 2 : return allPlot->byDistance() ? multiplier * current->stopKM : current->stop/60; // bottom right
case 3 : return allPlot->byDistance() ? multiplier * current->stopKM : current->stop/60; // bottom right
case 0 : return allPlot->bydist ? multiplier * current->startKM : current->start/60; // bottom left
case 1 : return allPlot->bydist ? multiplier * current->startKM : current->start/60; // top left
case 2 : return allPlot->bydist ? multiplier * current->stopKM : current->stop/60; // bottom right
case 3 : return allPlot->bydist ? multiplier * current->stopKM : current->stop/60; // bottom right
}
return 0; // shouldn't get here, but keeps compiler happy
}
@@ -784,3 +971,25 @@ size_t IntervalPlotData::size() const { return intervalCount()*4; }
QwtData *IntervalPlotData::copy() const {
return new IntervalPlotData(allPlot, mainWindow);
}
void
AllPlot::pointHover(QwtPlotCurve *curve, int index)
{
if (index >= 0 && curve != intervalHighlighterCurve) {
double value = curve->y(index);
// output the tooltip
QString text = QString("%1 %2")
.arg(value, 0, 'f', 0)
.arg(this->axisTitle(curve->yAxis()).text());
// set that text up
tooltip->setText(text);
} else {
// no point
tooltip->setText("");
}
}

View File

@@ -32,7 +32,10 @@ class AllPlotZoneLabel;
class AllPlotWindow;
class AllPlot;
class IntervalItem;
class IntervalPlotData;
class MainWindow;
class LTMToolTip;
class LTMCanvasPicker;
class AllPlot : public QwtPlot
{
@@ -40,19 +43,25 @@ class AllPlot : public QwtPlot
public:
AllPlot(QWidget *parent, MainWindow *mainWindow);
AllPlot(AllPlotWindow *parent, MainWindow *mainWindow);
int smoothing() const { return smooth; }
// set the curve data e.g. when a ride is selected
void setDataFromRide(RideItem *_rideItem);
void setDataFromPlot(AllPlot *plot, int startidx, int stopidx);
bool byDistance() const { return bydist; }
// convert from time/distance to index in *smoothed* datapoints
int timeIndex(double) const;
int distanceIndex(double) const;
bool useMetricUnits; // whether metric units are used (or imperial)
// plot redraw functions
bool shadeZones() const;
void refreshZoneLabels();
void refreshIntervalMarkers();
bool shadeZones() const;
void refreshZoneLabels();
void refreshIntervalMarkers();
void setData(RideItem *_rideItem);
// refresh data / plot parameters
void recalc();
void setYMax();
void setXTitle();
public slots:
@@ -62,32 +71,48 @@ class AllPlot : public QwtPlot
void showCad(int state);
void showAlt(int state);
void showGrid(int state);
void setShadeZones(bool x) { shade_zones=x; }
void setSmoothing(int value);
void setByDistance(int value);
void configChanged();
void pointHover(QwtPlotCurve*, int);
protected:
friend class ::AllPlotBackground;
friend class ::AllPlotZoneLabel;
friend class ::AllPlotWindow;
friend class ::IntervalPlotData;
AllPlotBackground *bg;
// cached state
RideItem *rideItem;
AllPlotBackground *bg;
QSettings *settings;
QVariant unit;
bool useMetricUnits;
// controls
bool shade_zones;
int showPowerState;
int showHrState;
int showSpeedState;
int showCadState;
int showAltState;
// plot objects
QwtPlotGrid *grid;
QVector<QwtPlotMarker*> d_mrk;
QwtPlotMarker *allMarker1;
QwtPlotMarker *allMarker2;
QwtPlotCurve *wattsCurve;
QwtPlotCurve *hrCurve;
QwtPlotCurve *speedCurve;
QwtPlotCurve *cadCurve;
QwtPlotCurve *altCurve;
QwtPlotCurve *intervalHighlighterCurve; // highlight selected intervals on the Plot
QVector<QwtPlotMarker*> d_mrk;
QList <AllPlotZoneLabel *> zoneLabels;
RideItem *rideItem;
QwtPlotGrid *grid;
QList <AllPlotZoneLabel *> zoneLabels;
// source data
QVector<double> hrArray;
QVector<double> wattsArray;
QVector<double> speedArray;
@@ -95,23 +120,26 @@ class AllPlot : public QwtPlot
QVector<double> timeArray;
QVector<double> distanceArray;
QVector<double> altArray;
// smoothed data
QVector<double> smoothWatts;
QVector<double> smoothHr;
QVector<double> smoothSpeed;
QVector<double> smoothCad;
QVector<double> smoothTime;
QVector<double> smoothDistance;
QVector<double> smoothAltitude;
// array / smooth state
int arrayLength;
int smooth;
bool bydist;
void recalc();
void setYMax();
void setXTitle();
bool shade_zones; // whether power should be shaded
int showPowerState;
int showHrState;
int showSpeedState;
int showCadState;
int showAltState;
private:
AllPlot *referencePlot;
AllPlotWindow *parent;
LTMToolTip *tooltip;
LTMCanvasPicker *_canvasPicker; // allow point selection/hover
};
#endif // _GC_AllPlot_h

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,13 @@ class QwtPlotPanner;
class QwtPlotZoomer;
class QwtPlotPicker;
class QwtPlotMarker;
class QwtArrowButton;
class RideItem;
class IntervalItem;
class QxtSpanSlider;
class QxtGroupBox;
#include "LTMWindow.h" // for tooltip/canvaspicker
class AllPlotWindow : public QWidget
{
@@ -38,20 +43,42 @@ class AllPlotWindow : public QWidget
AllPlotWindow(MainWindow *mainWindow);
void setData(RideItem *ride);
void setStartSelection(double seconds);
void setEndSelection(double seconds, bool newInterval, QString name);
// highlight a selection on the plots
void setStartSelection(AllPlot* plot, double xValue);
void setEndSelection(AllPlot* plot, double xValue, bool newInterval, QString name);
void clearSelection();
void hideSelection();
void zoomInterval(IntervalItem *); // zoom into a specified interval
// zoom to interval range (via span-slider)
void zoomInterval(IntervalItem *);
public slots:
void setSmoothingFromSlider();
void setSmoothingFromLineEdit();
// trap GC signals
void rideSelected();
void intervalSelected();
void zonesChanged();
void intervalsChanged();
void configChanged();
// trap child widget signals
void setSmoothingFromSlider();
void setSmoothingFromLineEdit();
void setStackZoomUp();
void setStackZoomDown();
void zoomChanged();
void moveLeft();
void moveRight();
void showStackChanged(int state);
void setShowPower(int state);
void setShowHr(int state);
void setShowSpeed(int state);
void setShowCad(int state);
void setShowAlt(int state);
void setShowGrid(int state);
void setSmoothing(int value);
void setByDistance(int value);
protected:
@@ -59,29 +86,63 @@ class AllPlotWindow : public QWidget
friend class IntervalPlotData;
friend class MainWindow;
void setAllPlotWidgets(RideItem *rideItem);
void setAllPlotWidgets(RideItem *rideItem);
// cached state
RideItem *current;
int selection;
MainWindow *mainWindow;
// All the plot widgets
AllPlot *allPlot;
AllPlot *fullPlot;
QList <AllPlot *> allPlots;
QwtPlotPanner *allPanner;
QwtPlotZoomer *allZoomer;
QwtPlotPicker *allPicker;
int selection;
QwtPlotMarker *allMarker1;
QwtPlotMarker *allMarker2;
QwtPlotMarker *allMarker3;
QCheckBox *showHr;
QCheckBox *showSpeed;
QCheckBox *showCad;
QCheckBox *showAlt;
QComboBox *showPower;
// Stacked view
QScrollArea *stackFrame;
QVBoxLayout *stackPlotLayout;
QWidget *stackWidget;
QwtArrowButton *stackZoomDown;
QwtArrowButton *stackZoomUp;
// Normal view
QScrollArea *allPlotFrame;
QPushButton *scrollLeft, *scrollRight;
// Common controls
QGridLayout *controlsLayout;
QCheckBox *showStack;
QCheckBox *showHr;
QCheckBox *showSpeed;
QCheckBox *showCad;
QCheckBox *showAlt;
QComboBox *showPower;
QSlider *smoothSlider;
QLineEdit *smoothLineEdit;
QxtSpanSlider *spanSlider;
private:
// reset/redraw all the plots
void setupStackPlots();
void redrawAllPlot();
void redrawFullPlot();
void redrawStackPlot();
void showInfo(QString);
void resetStackedDatas();
int stackWidth;
bool active;
bool stale;
private slots:
void addPickers(AllPlot *allPlot2);
bool stackZoomUpShouldEnable(int sw);
bool stackZoomDownShouldEnable(int sw);
void plotPickerMoved(const QPoint &);
void plotPickerSelected(const QPoint &);
};

View File

@@ -19,27 +19,33 @@
#include "RideMetric.h"
#include "Units.h"
#include <algorithm>
#define tr(s) QObject::tr(s)
#include <QApplication>
class WorkoutTime : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(WorkoutTime)
double seconds;
public:
WorkoutTime() : seconds(0.0) {}
QString symbol() const { return "workout_time"; }
QString name() const { return tr("Duration"); }
QString units(bool) const { return "seconds"; }
int precision() const { return 0; }
double value(bool) const { return seconds; }
void compute(const RideFile *ride, const Zones *, int,
WorkoutTime() : seconds(0.0)
{
setSymbol("workout_time");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Duration");
}
void initialize() {
#endif
setName(tr("Duration"));
setMetricUnits(tr("seconds"));
setImperialUnits(tr("seconds"));
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
seconds = ride->dataPoints().back()->secs -
ride->dataPoints().front()->secs + ride->recIntSecs();
ride->dataPoints().front()->secs + ride->recIntSecs();
setValue(seconds);
}
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) { seconds += other.value(true); }
RideMetric *clone() const { return new WorkoutTime(*this); }
};
@@ -49,31 +55,37 @@ static bool workoutTimeAdded =
//////////////////////////////////////////////////////////////////////////////
class TimeRiding : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(TimeRiding)
double secsMovingOrPedaling;
public:
TimeRiding() : secsMovingOrPedaling(0.0) {}
QString symbol() const { return "time_riding"; }
QString name() const { return tr("Time Riding"); }
QString units(bool) const { return "seconds"; }
int precision() const { return 0; }
double value(bool) const { return secsMovingOrPedaling; }
void compute(const RideFile *ride, const Zones *, int,
TimeRiding() : secsMovingOrPedaling(0.0)
{
setSymbol("time_riding");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Time Riding");
}
void initialize() {
#endif
setName(tr("Time Riding"));
setMetricUnits(tr("seconds"));
setImperialUnits(tr("seconds"));
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
secsMovingOrPedaling = 0;
foreach (const RideFilePoint *point, ride->dataPoints()) {
if ((point->kph > 0.0) || (point->cad > 0.0))
secsMovingOrPedaling += ride->recIntSecs();
}
setValue(secsMovingOrPedaling);
}
void override(const QMap<QString,QString> &map) {
if (map.contains("value"))
secsMovingOrPedaling = map.value("value").toDouble();
}
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
secsMovingOrPedaling += other.value(true);
}
RideMetric *clone() const { return new TimeRiding(*this); }
};
@@ -83,28 +95,37 @@ static bool timeRidingAdded =
//////////////////////////////////////////////////////////////////////////////
class TotalDistance : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(TotalDistance)
double km;
public:
TotalDistance() : km(0.0) {}
QString symbol() const { return "total_distance"; }
QString name() const { return tr("Distance"); }
QString units(bool metric) const { return metric ? "km" : "miles"; }
int precision() const { return 1; }
double value(bool metric) const {
return metric ? km : (km * MILES_PER_KM);
TotalDistance() : km(0.0)
{
setSymbol("total_distance");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Distance");
}
void compute(const RideFile *ride, const Zones *, int,
void initialize() {
#endif
setName(tr("Distance"));
setType(RideMetric::Total);
setMetricUnits(tr("km"));
setImperialUnits(tr("miles"));
setPrecision(1);
setConversion(MILES_PER_KM);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
// Note: The 'km' in each sample is the distance travelled by the
// *end* of the sampling period. The last term in this equation
// accounts for the distance traveled *during* the first sample.
km = ride->dataPoints().back()->km - ride->dataPoints().front()->km
+ ride->dataPoints().front()->kph / 3600.0 * ride->recIntSecs();
setValue(km);
}
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) { km += other.value(true); }
RideMetric *clone() const { return new TotalDistance(*this); }
};
@@ -115,20 +136,28 @@ static bool totalDistanceAdded =
class ElevationGain : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(ElevationGain)
double elegain;
double prevalt;
public:
ElevationGain() : elegain(0.0), prevalt(0.0) {}
QString symbol() const { return "elevation_gain"; }
QString name() const { return tr("Elevation Gain"); }
QString units(bool metric) const { return metric ? "meters" : "feet"; }
int precision() const { return 0; }
double value(bool metric) const {
return metric ? elegain : (elegain * FEET_PER_METER);
ElevationGain() : elegain(0.0), prevalt(0.0)
{
setSymbol("elevation_gain");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Elevation Gain");
}
void compute(const RideFile *ride, const Zones *, int,
void initialize() {
#endif
setName(tr("Elevation Gain"));
setType(RideMetric::Total);
setMetricUnits(tr("meters"));
setImperialUnits(tr("feet"));
setConversion(FEET_PER_METER);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
const double hysteresis = 3.0;
bool first = true;
@@ -145,10 +174,7 @@ class ElevationGain : public RideMetric {
prevalt = point->alt;
}
}
}
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
elegain += other.value(true);
setValue(elegain);
}
RideMetric *clone() const { return new ElevationGain(*this); }
};
@@ -159,28 +185,31 @@ static bool elevationGainAdded =
//////////////////////////////////////////////////////////////////////////////
class TotalWork : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(TotalWork)
double joules;
public:
TotalWork() : joules(0.0) {}
QString symbol() const { return "total_work"; }
QString name() const { return tr("Work"); }
QString units(bool) const { return "kJ"; }
int precision() const { return 0; }
double value(bool) const { return joules / 1000.0; }
void compute(const RideFile *ride, const Zones *, int,
TotalWork() : joules(0.0)
{
setSymbol("total_work");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Work");
}
void initialize() {
#endif
setName(tr("Work"));
setMetricUnits(tr("kJ"));
setImperialUnits(tr("kJ"));
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->watts >= 0.0)
joules += point->watts * ride->recIntSecs();
}
}
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
const TotalWork &tw = dynamic_cast<const TotalWork&>(other);
joules += tw.joules;
setValue(joules/1000);
}
RideMetric *clone() const { return new TotalWork(*this); }
};
@@ -191,34 +220,45 @@ static bool totalWorkAdded =
//////////////////////////////////////////////////////////////////////////////
class AvgSpeed : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(AvgSpeed)
double secsMoving;
double km;
public:
AvgSpeed() : secsMoving(0.0), km(0.0) {}
QString symbol() const { return "average_speed"; }
QString name() const { return tr("Average Speed"); }
QString units(bool metric) const { return metric ? "kph" : "mph"; }
int precision() const { return 1; }
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);
AvgSpeed() : secsMoving(0.0), km(0.0)
{
setSymbol("average_speed");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Average Speed");
}
void compute(const RideFile *ride, const Zones *, int,
void initialize() {
#endif
setName(tr("Average Speed"));
setMetricUnits(tr("km/h"));
setImperialUnits(tr("mph"));
setType(RideMetric::Average);
setPrecision(1);
setConversion(MILES_PER_KM);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &deps) {
assert(deps.contains("total_distance"));
km = deps.value("total_distance")->value(true);
foreach (const RideFilePoint *point, ride->dataPoints())
if (point->kph > 0.0) secsMoving += ride->recIntSecs();
setValue(secsMoving ? km / secsMoving * 3600.0 : 0.0);
}
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
const AvgSpeed &as = dynamic_cast<const AvgSpeed&>(other);
secsMoving += as.secsMoving;
km += as.km;
setValue(secsMoving ? km / secsMoving * 3600.0 : 0.0);
}
RideMetric *clone() const { return new AvgSpeed(*this); }
};
@@ -229,20 +269,37 @@ static bool avgSpeedAdded =
//////////////////////////////////////////////////////////////////////////////
struct AvgPower : public AvgRideMetric {
class AvgPower : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(AvgPower)
QString symbol() const { return "average_power"; }
QString name() const { return tr("Average Power"); }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
void compute(const RideFile *ride, const Zones *, int,
double count, total;
public:
AvgPower()
{
setSymbol("average_power");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Average Power");
}
void initialize() {
#endif
setName(tr("Average Power"));
setMetricUnits(tr("watts"));
setImperialUnits(tr("watts"));
setType(RideMetric::Average);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
total = count = 0;
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->watts >= 0.0) {
total += point->watts;
++count;
}
}
setValue(count > 0 ? total / count : 0);
setCount(count);
}
RideMetric *clone() const { return new AvgPower(*this); }
};
@@ -252,20 +309,37 @@ static bool avgPowerAdded =
//////////////////////////////////////////////////////////////////////////////
struct AvgHeartRate : public AvgRideMetric {
class AvgHeartRate : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(AvgHeartRate)
QString symbol() const { return "average_hr"; }
QString name() const { return tr("Average Heart Rate"); }
QString units(bool) const { return "bpm"; }
int precision() const { return 0; }
void compute(const RideFile *ride, const Zones *, int,
double total, count;
public:
AvgHeartRate()
{
setSymbol("average_hr");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Average Heart Rate");
}
void initialize() {
#endif
setName(tr("Average Heart Rate"));
setMetricUnits(tr("bpm"));
setImperialUnits(tr("bpm"));
setType(RideMetric::Average);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
total = count = 0;
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->hr > 0) {
total += point->hr;
++count;
}
}
setValue(count > 0 ? total / count : 0);
setCount(count);
}
RideMetric *clone() const { return new AvgHeartRate(*this); }
};
@@ -275,20 +349,37 @@ static bool avgHeartRateAdded =
//////////////////////////////////////////////////////////////////////////////
struct AvgCadence : public AvgRideMetric {
class AvgCadence : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(AvgCadence)
QString symbol() const { return "average_cad"; }
QString name() const { return tr("Average Cadence"); }
QString units(bool) const { return "rpm"; }
int precision() const { return 0; }
void compute(const RideFile *ride, const Zones *, int,
double total, count;
public:
AvgCadence()
{
setSymbol("average_cad");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Average Cadence");
}
void initialize() {
#endif
setName(tr("Average Cadence"));
setMetricUnits(tr("rpm"));
setImperialUnits(tr("rpm"));
setType(RideMetric::Average);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
total = count = 0;
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->cad > 0) {
total += point->cad;
++count;
}
}
setValue(count > 0 ? total / count : count);
setCount(count);
}
RideMetric *clone() const { return new AvgCadence(*this); }
};
@@ -299,20 +390,30 @@ static bool avgCadenceAdded =
//////////////////////////////////////////////////////////////////////////////
class MaxPower : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(MaxPower)
double max;
public:
MaxPower() : max(0.0) {}
QString symbol() const { return "max_power"; }
QString name() const { return tr("Max Power"); }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
double value(bool) const { return max; }
void compute(const RideFile *ride, const Zones *, int,
MaxPower() : max(0.0)
{
setSymbol("max_power");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Max Power");
}
void initialize() {
#endif
setName(tr("Max Power"));
setMetricUnits(tr("watts"));
setImperialUnits(tr("watts"));
setType(RideMetric::Peak);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->watts >= max)
max = point->watts;
}
setValue(max);
}
RideMetric *clone() const { return new MaxPower(*this); }
};
@@ -322,16 +423,59 @@ static bool maxPowerAdded =
//////////////////////////////////////////////////////////////////////////////
class MaxHr : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(MaxHr)
double max;
public:
MaxHr() : max(0.0)
{
setSymbol("max_heartrate");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Max Heartrate");
}
void initialize() {
#endif
setName(tr("Max Heartrate"));
setMetricUnits(tr("bpm"));
setImperialUnits(tr("bpm"));
setType(RideMetric::Peak);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
foreach (const RideFilePoint *point, ride->dataPoints()) {
if (point->hr >= max)
max = point->hr;
}
setValue(max);
}
RideMetric *clone() const { return new MaxHr(*this); }
};
static bool maxHrAdded =
RideMetricFactory::instance().addMetric(MaxHr());
//////////////////////////////////////////////////////////////////////////////
class NinetyFivePercentHeartRate : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(NinetyFivePercentHeartRate)
double hr;
public:
NinetyFivePercentHeartRate() : hr(0.0) {}
QString symbol() const { return "ninety_five_percent_hr"; }
QString name() const { return tr("95% Heart Rate"); }
QString units(bool) const { return "bpm"; }
int precision() const { return 0; }
double value(bool) const { return hr; }
void compute(const RideFile *ride, const Zones *, int,
NinetyFivePercentHeartRate() : hr(0.0)
{
setSymbol("ninety_five_percent_hr");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("95% Heartrate");
}
void initialize() {
#endif
setName(tr("95% Heartrate"));
setMetricUnits(tr("bpm"));
setImperialUnits(tr("bpm"));
setType(RideMetric::Average);
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
QVector<double> hrs;
foreach (const RideFilePoint *point, ride->dataPoints()) {
@@ -342,6 +486,7 @@ class NinetyFivePercentHeartRate : public RideMetric {
std::sort(hrs.begin(), hrs.end());
hr = hrs[hrs.size() * 0.95];
}
setValue(hr);
}
RideMetric *clone() const { return new NinetyFivePercentHeartRate(*this); }
};

View File

@@ -267,7 +267,6 @@ BestIntervalDialog::findBests(const RideFile *ride, double windowSizeSecs,
totalWatts += point->watts;
window.append(point);
double duration = intervalDuration(window.first(), window.last(), ride);
assert(duration < windowSizeSecs + secsDelta);
if (duration >= windowSizeSecs) {
double start = window.first()->secs;
double stop = start + duration;

View File

@@ -19,8 +19,7 @@
#include "RideMetric.h"
#include "Zones.h"
#include <math.h>
#define tr(s) QObject::tr(s)
#include <QApplication>
const double bikeScoreN = 4.0;
@@ -34,21 +33,28 @@ const double bikeScoreN = 4.0;
// a spreadsheet provided by Dr. Skiba.
class XPower : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(XPower)
double xpower;
double secs;
friend class RelativeIntensity;
friend class BikeScore;
public:
XPower() : xpower(0.0), secs(0.0) {}
QString symbol() const { return "skiba_xpower"; }
QString name() const { return tr("xPower"); }
QString units(bool) const { return "watts"; }
int precision() const { return 0; }
double value(bool) const { return xpower; }
void compute(const RideFile *ride, const Zones *, int,
XPower() : xpower(0.0), secs(0.0)
{
setSymbol("skiba_xpower");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("xPower");
}
void initialize() {
#endif
setName(tr("xPower"));
setType(RideMetric::Average);
setMetricUnits(tr("watts"));
setImperialUnits(tr("watts"));
}
void compute(const RideFile *ride, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &) {
static const double EPSILON = 0.1;
@@ -81,53 +87,96 @@ class XPower : public RideMetric {
}
xpower = pow(total / count, 0.25);
secs = count * secsDelta;
}
// added djconnel: allow RI to be combined across rides
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
const XPower &ap = dynamic_cast<const XPower&>(other);
xpower = pow(xpower, bikeScoreN) * secs + pow(ap.xpower, bikeScoreN) * ap.secs;
secs += ap.secs;
xpower = pow(xpower / secs, 1 / bikeScoreN);
setValue(xpower);
setCount(secs);
}
// end added djconnel
RideMetric *clone() const { return new XPower(*this); }
};
class VariabilityIndex : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(VariabilityIndex)
double vi;
double secs;
public:
VariabilityIndex() : vi(0.0), secs(0.0)
{
setSymbol("skiba_variability_index");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Skiba VI");
}
void initialize() {
#endif
setName(tr("Skiba VI"));
setType(RideMetric::Average);
setMetricUnits(tr(""));
setImperialUnits(tr(""));
setPrecision(3);
}
void compute(const RideFile *, const Zones *, int, const HrZones *, int,
const QHash<QString,RideMetric*> &deps) {
assert(deps.contains("skiba_xpower"));
assert(deps.contains("average_power"));
XPower *xp = dynamic_cast<XPower*>(deps.value("skiba_xpower"));
assert(xp);
RideMetric *ap = dynamic_cast<RideMetric*>(deps.value("average_power"));
assert(ap);
vi = xp->value(true) / ap->value(true);
secs = xp->count();
setValue(vi);
}
RideMetric *clone() const { return new VariabilityIndex(*this); }
};
class RelativeIntensity : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(RelativeIntensity)
double reli;
double secs;
public:
RelativeIntensity() : reli(0.0), secs(0.0) {}
QString symbol() const { return "skiba_relative_intensity"; }
QString name() const { return tr("Relative Intensity"); }
QString units(bool) const { return ""; }
int precision() const { return 3; }
double value(bool) const { return reli; }
void compute(const RideFile *, const Zones *zones, int zoneRange,
RelativeIntensity() : reli(0.0), secs(0.0)
{
setSymbol("skiba_relative_intensity");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Relative Intensity");
}
void initialize() {
#endif
setName(tr("Relative Intensity"));
setType(RideMetric::Average);
setMetricUnits(tr(""));
setImperialUnits(tr(""));
setPrecision(3);
}
void compute(const RideFile *, const Zones *zones, int zoneRange, const HrZones *, int,
const QHash<QString,RideMetric*> &deps) {
if (zones && zoneRange >= 0) {
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;
reli = xp->value(true) / zones->getCP(zoneRange);
secs = xp->count();
}
setValue(reli);
setCount(secs);
}
// added djconnel: allow RI to be combined across rides
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) {
assert(symbol() == other.symbol());
const RelativeIntensity &ap = dynamic_cast<const RelativeIntensity&>(other);
reli = secs * pow(reli, bikeScoreN) + ap.secs * pow(ap.reli, bikeScoreN);
secs += ap.secs;
reli = pow(reli / secs, 1.0 / bikeScoreN);
const RelativeIntensity &ap = dynamic_cast<const RelativeIntensity&>(other);
reli = secs * pow(reli, bikeScoreN) + ap.count() * pow(ap.value(true), bikeScoreN);
secs += ap.count();
reli = pow(reli / secs, 1.0 / bikeScoreN);
setValue(reli);
}
// end added djconnel
@@ -135,48 +184,58 @@ class RelativeIntensity : public RideMetric {
};
class BikeScore : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(BikeScore)
double score;
public:
BikeScore() : score(0.0) {}
QString symbol() const { return "skiba_bike_score"; }
QString name() const { return tr("BikeScore&#8482;"); }
QString units(bool) const { return ""; }
int precision() const { return 0; }
double value(bool) const { return score; }
void compute(const RideFile *, const Zones *zones, int zoneRange,
BikeScore() : score(0.0)
{
setSymbol("skiba_bike_score");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("BikeScore&#8482;");
}
void initialize() {
#endif
setName(tr("BikeScore&#8482;"));
setMetricUnits("");
setImperialUnits("");
}
void compute(const RideFile *, const Zones *zones, int zoneRange,const HrZones *, int,
const QHash<QString,RideMetric*> &deps) {
if (!zones || zoneRange < 0)
return;
if (!zones || zoneRange < 0)
return;
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 normWork = xp->value(true) * xp->count();
double rawBikeScore = normWork * ri->value(true);
double workInAnHourAtCP = zones->getCP(zoneRange) * 3600;
score = rawBikeScore / workInAnHourAtCP * 100.0;
setValue(score);
}
void override(const QMap<QString,QString> &map) {
if (map.contains("value"))
score = map.value("value").toDouble();
}
RideMetric *clone() const { return new BikeScore(*this); }
bool canAggregate() const { return true; }
void aggregateWith(const RideMetric &other) { score += other.value(true); }
};
static bool addAllThree() {
static bool addAllFour() {
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);
deps.clear();
deps.append("skiba_xpower");
deps.append("average_power");
RideMetricFactory::instance().addMetric(VariabilityIndex(), &deps);
return true;
}
static bool allThreeAdded = addAllThree();
static bool allFourAdded = addAllFour();

768
src/BinRideFile.cpp Normal file
View File

@@ -0,0 +1,768 @@
/*
* Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch)
*
* 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 "BinRideFile.h"
#include <QSharedPointer>
#include <QMap>
#include <QSet>
#include <assert.h>
#include <stdio.h>
#include <stdint.h>
#define RECORD_TYPE__META 0
#define RECORD_TYPE__RIDE_DATA 1
#define RECORD_TYPE__RAW_DATA 2
#define RECORD_TYPE__SPARSE_DATA 3
#define RECORD_TYPE__INTERVAL_DATA 4
#define RECORD_TYPE__DATA_ERROR 5
#define RECORD_TYPE__HISTORY 6
#define FORMAT_ID__RIDE_START 0
#define FORMAT_ID__RIDE_SAMPLE_RATE 1
#define FORMAT_ID__FIRMWARE_VERSION 2
#define FORMAT_ID__LAST_UPDATE 3
#define FORMAT_ID__ODOMETER 4
#define FORMAT_ID__PRIMARY_POWER_ID 5
#define FORMAT_ID__SECONDARY_POWER_ID 6
#define FORMAT_ID__CHEST_STRAP_ID 7
#define FORMAT_ID__CADENCE_ID 8
#define FORMAT_ID__SPEED_ID 9
#define FORMAT_ID__RESISTANCE_UNITID 10
#define FORMAT_ID__WORKOUT_ID 11
#define FORMAT_ID__USER_WEIGHT 12
#define FORMAT_ID__USER_CATEGORY 13
#define FORMAT_ID__USER_HR_ZONE_1 14
#define FORMAT_ID__USER_HR_ZONE_2 15
#define FORMAT_ID__USER_HR_ZONE_3 16
#define FORMAT_ID__USER_HR_ZONE_4 17
#define FORMAT_ID__USER_POWER_ZONE_1 18
#define FORMAT_ID__USER_POWER_ZONE_2 19
#define FORMAT_ID__USER_POWER_ZONE_3 20
#define FORMAT_ID__USER_POWER_ZONE_4 21
#define FORMAT_ID__USER_POWER_ZONE_5 22
#define FORMAT_ID__WHEEL_CIRC 23
#define FORMAT_ID__RIDE_DISTANCE 24
#define FORMAT_ID__RIDE_TIME 25
#define FORMAT_ID__POWER 26
#define FORMAT_ID__TORQUE 27
#define FORMAT_ID__SPEED 28
#define FORMAT_ID__CADENCE 29
#define FORMAT_ID__HEART_RATE 30
#define FORMAT_ID__GRADE 31
#define FORMAT_ID__ALTITUDE_OLD 32
#define FORMAT_ID__RAW_DATA 33
#define FORMAT_ID__TEMPERATURE 34
#define FORMAT_ID__INTERVAL_NUM 35
#define FORMAT_ID__DROPOUT_FLAGS 36
#define FORMAT_ID__RAW_DATA_FORMAT 37
#define FORMAT_ID__RAW_BARO_SENSOR 38
#define FORMAT_ID__ALTITUDE 39
#define FORMAT_ID__THRESHOLD_POWER 40
#define FORMAT_ID__UNKNOW_41 41
#define FORMAT_ID__UNKNOW_42 42
#define FORMAT_ID__UNKNOW_43 43
#define FORMAT_ID__UNKNOW_44 44
#define FORMAT_ID__UNKNOW_45 45
#define FORMAT_ID__UNKNOW_46 46
#define FORMAT_ID__UNKNOW_47 47
static int binFileReaderRegistered =
RideFileFactory::instance().registerReader(
"bin", "Joule Bin File", new BinFileReader());
struct BinField {
int num;
int id;
int size; // in bytes
};
struct BinDefinition {
int format_identifier;
std::vector<BinField> fields;
};
struct BinFileReaderState
{
static QMap<int,QString> global_record_types;
static QMap<int,QString> global_format_identifiers;
QMap<int, BinDefinition> local_format_identifiers;
QSet<int> unknown_record_types, unknown_format_identifiers;
QSet<int> unused_record_types;
QSet<int> unexpected_record_types;
QMap<int, QSet<int> > unknown_format_identifiers_for_record_types;
QMap<int, QSet<int> > unused_format_identifiers_for_record_types;
QMap<int, QSet<int> > unexpected_format_identifiers_for_record_types;
QFile &file;
QStringList &errors;
RideFile *rideFile;
time_t start_time;
int interval;
double last_interval_secs;
bool stopped;
BinFileReaderState(QFile &file, QStringList &errors) :
file(file), errors(errors), rideFile(NULL), start_time(0),
interval(0), last_interval_secs(0.0), stopped(true)
{
if (global_record_types.isEmpty()) {
global_record_types.insert(RECORD_TYPE__META, "Meta Data");
global_record_types.insert(RECORD_TYPE__RIDE_DATA, "1 sec detail ride data");
global_record_types.insert(RECORD_TYPE__RAW_DATA, "Raw data packet");
global_record_types.insert(RECORD_TYPE__SPARSE_DATA, "Sparse ride data");
global_record_types.insert(RECORD_TYPE__INTERVAL_DATA, "Interval");
global_record_types.insert(RECORD_TYPE__DATA_ERROR, "Data error");
global_record_types.insert(RECORD_TYPE__HISTORY, "History summary record");
}
if (global_format_identifiers.isEmpty()) {
global_format_identifiers.insert(FORMAT_ID__RIDE_START, "Ride start");
global_format_identifiers.insert(FORMAT_ID__RIDE_SAMPLE_RATE, "Ride sample rate");
global_format_identifiers.insert(FORMAT_ID__FIRMWARE_VERSION, "Firmware Version");
global_format_identifiers.insert(FORMAT_ID__LAST_UPDATE, "Last update");
global_format_identifiers.insert(FORMAT_ID__ODOMETER, "Odometer");
global_format_identifiers.insert(FORMAT_ID__PRIMARY_POWER_ID, "Primary Power Radio ID");
global_format_identifiers.insert(FORMAT_ID__SECONDARY_POWER_ID, "Secondary Power Radio ID");
global_format_identifiers.insert(FORMAT_ID__CHEST_STRAP_ID, "Chest strap Radio ID");
global_format_identifiers.insert(FORMAT_ID__CADENCE_ID, "Cadense sensor Radio ID");
global_format_identifiers.insert(FORMAT_ID__SPEED_ID, "Speed sensor Radio ID");
global_format_identifiers.insert(FORMAT_ID__RESISTANCE_UNITID, "Resistance Unit Radio ID");
global_format_identifiers.insert(FORMAT_ID__WORKOUT_ID, "Workout ID");
global_format_identifiers.insert(FORMAT_ID__USER_WEIGHT, "User weight");
global_format_identifiers.insert(FORMAT_ID__USER_CATEGORY, "User training category");
global_format_identifiers.insert(FORMAT_ID__USER_HR_ZONE_1, "User HR Zone 1");
global_format_identifiers.insert(FORMAT_ID__USER_HR_ZONE_2, "User HR Zone 2");
global_format_identifiers.insert(FORMAT_ID__USER_HR_ZONE_3, "User HR Zone 3");
global_format_identifiers.insert(FORMAT_ID__USER_HR_ZONE_4, "User HR Zone 4");
global_format_identifiers.insert(FORMAT_ID__USER_POWER_ZONE_1, "User Power Zone 1");
global_format_identifiers.insert(FORMAT_ID__USER_POWER_ZONE_2, "User Power Zone 2");
global_format_identifiers.insert(FORMAT_ID__USER_POWER_ZONE_3, "User Power Zone 3");
global_format_identifiers.insert(FORMAT_ID__USER_POWER_ZONE_4, "User Power Zone 4");
global_format_identifiers.insert(FORMAT_ID__USER_POWER_ZONE_5, "User Power Zone 5");
global_format_identifiers.insert(FORMAT_ID__WHEEL_CIRC, "Wheel circumference");
global_format_identifiers.insert(FORMAT_ID__RIDE_DISTANCE, "Ride distance");
global_format_identifiers.insert(FORMAT_ID__RIDE_TIME, "Ride time");
global_format_identifiers.insert(FORMAT_ID__POWER, "Power");
global_format_identifiers.insert(FORMAT_ID__TORQUE, "Torque");
global_format_identifiers.insert(FORMAT_ID__SPEED, "Speed");
global_format_identifiers.insert(FORMAT_ID__CADENCE, "Cadence");
global_format_identifiers.insert(FORMAT_ID__HEART_RATE, "Heart rate");
global_format_identifiers.insert(FORMAT_ID__GRADE, "Grade");
global_format_identifiers.insert(FORMAT_ID__ALTITUDE_OLD, "Altitude");
global_format_identifiers.insert(FORMAT_ID__RAW_DATA, "Raw Data");
global_format_identifiers.insert(FORMAT_ID__TEMPERATURE, "Temperature");
global_format_identifiers.insert(FORMAT_ID__INTERVAL_NUM, "Interval number");
global_format_identifiers.insert(FORMAT_ID__DROPOUT_FLAGS, "Dropout flags");
global_format_identifiers.insert(FORMAT_ID__RAW_DATA_FORMAT, "Raw Data Format ID");
global_format_identifiers.insert(FORMAT_ID__RAW_BARO_SENSOR, "Raw Baro Sensor Value");
global_format_identifiers.insert(FORMAT_ID__ALTITUDE, "Altitude");
global_format_identifiers.insert(FORMAT_ID__THRESHOLD_POWER, "Threshold power");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_41, "Unknow 41");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_42, "Unknow 42");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_43, "Unknow 43");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_44, "Unknow 44");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_45, "Unknow 45");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_46, "Unknow 46");
global_format_identifiers.insert(FORMAT_ID__UNKNOW_47, "Unknow 47");
}
}
int read_byte(int *count = NULL, int *sum = NULL) {
char c;
if (file.read(&c, 1) != 1)
return -1;
if (sum)
*sum += (0xff & c);
if (count)
*count += 1;
return 0xff & c;
}
int read_double_byte(int *count = NULL, int *sum = NULL) {
char c1,c2;
if (file.read(&c1, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c2, 1) != 1)
return -1;
if (count)
*count += 1;
if (sum)
*sum += (0xff & c1) + (0xff & c2);
return 256*(0xff & c1) + (0xff & c2);
}
int read_four_byte(int *count = NULL, int *sum = NULL) {
char c1,c2,c3,c4;
if (file.read(&c1, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c2, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c3, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c4, 1) != 1)
return -1;
if (count)
*count += 1;
if (sum)
*sum += (0xff & c1) + (0xff & c2) + (0xff & c3) + (0xff & c4);
return 256*256*256*(0xff & c1) + 256*256*(0xff & c2) + 256*(0xff & c3) + (0xff & c4);
}
int read_date(int *count = NULL, int *sum = NULL) {
char c1,c2,c3,c4,c5,c6,c7;
if (file.read(&c1, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c2, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c3, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c4, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c5, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c6, 1) != 1)
return -1;
if (count)
*count += 1;
if (file.read(&c7, 1) != 1)
return -1;
if (count)
*count += 1;
if (sum)
*sum += (0xff & c1) + (0xff & c2) + (0xff & c3) + (0xff & c4) + (0xff & c5) + (0xff & c6) + (0xff & c7);
QDateTime dateTime(QDate((0xff & c1)*256+(0xff & c2), (0xff & c3), (0xff & c4)), QTime((0xff & c5), (0xff & c6), (0xff & c7)), Qt::LocalTime);
return dateTime.toTime_t();
}
void decodeMetaData(const BinDefinition &def, const std::vector<int> values) {
int i = 0;
QString deviceInfo = "";
QDateTime t;
foreach(const BinField &field, def.fields) {
if (!global_format_identifiers.contains(field.id)) {
unknown_format_identifiers.insert(field.id);
} else {
int value = values[i++];
switch (field.id) {
case FORMAT_ID__RIDE_START : {
start_time = value;
t.setTime_t(value);
rideFile->setStartTime(t);
break;
}
case FORMAT_ID__RIDE_SAMPLE_RATE :
rideFile->setRecIntSecs(value/1000.0);
break;
case FORMAT_ID__FIRMWARE_VERSION :
//rideFile->setDeviceType(rideFile->deviceType()+ QString(" (%1)").arg(value));
deviceInfo += rideFile->deviceType()+QString(" Version %1\n").arg(value);
break;
case FORMAT_ID__LAST_UPDATE :
//t.setTime_t(value);
//deviceInfo += QString("Last update %1\n").arg(t.toString());
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__ODOMETER :
if (value>0)
deviceInfo += QString("Odometer %1km\n").arg(value/10.0);
break;
case FORMAT_ID__PRIMARY_POWER_ID :
if (value>0)
deviceInfo += QString("Primary Power Id %1\n").arg(value);
break;
case FORMAT_ID__SECONDARY_POWER_ID :
if (value>0)
deviceInfo += QString("Secondary Power Id %1\n").arg(value);
break;
case FORMAT_ID__CHEST_STRAP_ID :
if (value>0)
deviceInfo += QString("Chest strap Id %1\n").arg(value);
break;
case FORMAT_ID__CADENCE_ID :
if (value>0)
deviceInfo += QString("Cadence Id %1\n").arg(value);
break;
case FORMAT_ID__SPEED_ID :
if (value>0)
deviceInfo += QString("Speed Id %1\n").arg(value);
break;
case FORMAT_ID__RESISTANCE_UNITID :
if (value>0)
deviceInfo += QString("Resistance Unit Id %1\n").arg(value);
break;
case FORMAT_ID__WORKOUT_ID :
rideFile->setTag("Workout Code", QString(value));
break;
case FORMAT_ID__USER_WEIGHT :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_CATEGORY :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_HR_ZONE_1 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_HR_ZONE_2 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_HR_ZONE_3 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_HR_ZONE_4 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_POWER_ZONE_1 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_POWER_ZONE_2 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_POWER_ZONE_3 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_POWER_ZONE_4 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__USER_POWER_ZONE_5 :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__WHEEL_CIRC :
deviceInfo += QString("Wheel Circ. %1mm\n").arg(value);
break;
case FORMAT_ID__THRESHOLD_POWER :
deviceInfo += QString("Threshold Power %1W\n").arg(value);
break;
case FORMAT_ID__UNKNOW_41 :
break;
case FORMAT_ID__UNKNOW_42 :
break;
case FORMAT_ID__UNKNOW_43 :
break;
default:
unexpected_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
}
}
}
rideFile->setTag("Device Info", deviceInfo);
}
void decodeRideData(const BinDefinition &def, const std::vector<int> values) {
int i = 0;
double secs = 0, alt = 0, cad = 0, km = 0, grade = 0, hr = 0;
double nm = 0, kph = 0, watts = 0;
foreach(const BinField &field, def.fields) {
if (!global_format_identifiers.contains(field.id)) {
unknown_format_identifiers.insert(field.id);
} else {
int value = values[i++];
switch (field.id) {
case FORMAT_ID__RIDE_DISTANCE :
km = value/10000.0;
case FORMAT_ID__RIDE_TIME :
secs = value / 1000.0;
break;
case FORMAT_ID__POWER :
if (value <= 2999) // Limit from definition
watts = value;
break;
case FORMAT_ID__TORQUE :
nm = value;
break;
case FORMAT_ID__SPEED :
value = value*3.6/100.0;
if (value < 145) // Limit for data error
kph = value;
break;
case FORMAT_ID__CADENCE :
if (value < 255) // Limit for data error
cad = value;
break;
case FORMAT_ID__HEART_RATE :
if (value < 255) // Limit for data error
hr = value;
break;
case FORMAT_ID__GRADE :
grade = value;
break;
case FORMAT_ID__ALTITUDE :
alt = value/10.0;
break;
case FORMAT_ID__ALTITUDE_OLD :
alt = value/10.0;
break;
case FORMAT_ID__UNKNOW_44 :
break;
case FORMAT_ID__UNKNOW_45 :
break;
case FORMAT_ID__UNKNOW_46 :
break;
case FORMAT_ID__UNKNOW_47 :
break;
default:
unexpected_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
}
}
}
double headwind = 0.0;
int interval = 0;
int lng = 0;
int lat = 0;
rideFile->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, lng, lat, headwind, interval);
//printf("addPoint time %f hr %f speed %f dist %f alt %f\n", secs, hr, kph, km, alt);
}
void decodeSparseData(const BinDefinition &def, const std::vector<int> values) {
int i = 0;
int temperature_count = 0;
double temperature = 0.0;
foreach(const BinField &field, def.fields) {
if (!global_format_identifiers.contains(field.id)) {
unknown_format_identifiers.insert(field.id);
} else {
int value = values[i++];
switch (field.id) {
case FORMAT_ID__TEMPERATURE :
// use for average
temperature += value/10.0;
temperature_count ++;
break;
case FORMAT_ID__RIDE_TIME :
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
default:
unexpected_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
}
}
}
rideFile->setTag("Temperature", QString("%1").arg(temperature/temperature_count));
}
void decodeIntervalData(const BinDefinition &def, const std::vector<int> values) {
int i = 0;
double secs = 0;
foreach(const BinField &field, def.fields) {
if (!global_format_identifiers.contains(field.id)) {
unknown_format_identifiers.insert(field.id);
} else {
int value = values[i++];
switch (field.id) {
case FORMAT_ID__INTERVAL_NUM :
interval = value;
break;
case FORMAT_ID__RIDE_TIME :
secs = value / 1000.0;;
break;
default:
unexpected_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
}
}
}
if (interval>1) {
rideFile->addInterval(last_interval_secs, secs, QString("%1").arg(interval-1));
}
last_interval_secs = secs;
}
void decodeDataError(const BinDefinition &def, const std::vector<int> values) {
int i = 0;
foreach(const BinField &field, def.fields) {
if (!global_format_identifiers.contains(field.id)) {
unknown_format_identifiers.insert(field.id);
} else {
int value = values[i++];
bool b0 = (value % 2);
bool b1 = (value / 2>1);
bool b2 = (value / 4>1);
bool b3 = (value / 8>1);
bool b4 = (value / 16>1);
bool b5 = (value / 32>1);
bool b6 = (value / 64>1);
bool b7 = (value / 128>1);
QString b = QString("DataError : %1 %2 %3 %4 %5 %6 %7 %8").arg(b0?"0":"").arg(b1?"1":"").arg(b2?"2":"").arg(b3?"3":"").arg(b4?"4":"").arg(b5?"5":"").arg(b6?"6":"").arg(b7?"7":"");
switch (field.id) {
case FORMAT_ID__DROPOUT_FLAGS :
//errors << QString("DataError field.id %1 value %2").arg(field.id).arg(b);
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
case FORMAT_ID__RIDE_TIME :
//errors << QString("DataError time field.id %1 value %2").arg(field.id).arg(value/1000);
unused_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
break;
default:
unexpected_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
}
}
}
}
void decodeHistoryData(const BinDefinition &def, const std::vector<int> /*values*/) {
//int i = 0;
foreach(const BinField &field, def.fields) {
if (!global_format_identifiers.contains(field.id)) {
unknown_format_identifiers.insert(field.id);
} else {
//int value = values[i++];
switch (field.id) {
default:
unexpected_format_identifiers_for_record_types[def.format_identifier].insert(field.id);
}
}
}
}
int read_record(bool &stop, QStringList &errors) {
int sum = 0;
int bytes_read = 0;
int record_type = read_byte(&bytes_read, &sum); // Always 0xFF
if (record_type == -1) {
errors << QString("Truncated file");
//bytes_read++;
return bytes_read;
} else if (record_type == 255) {
int format_identifier = read_byte(&bytes_read, &sum);
if (format_identifier == -1) {
errors << QString("Truncated file");
//bytes_read++;
return bytes_read;
} else if (!global_record_types.contains(format_identifier)) {
errors << QString("unknown format_identifier %1").arg(format_identifier);
stop = true;
return bytes_read;
}
int nb_meta = read_double_byte(&bytes_read, &sum);
local_format_identifiers.insert(format_identifier, BinDefinition());
//printf("- format_identifier : %d\n", format_identifier);
BinDefinition &def = local_format_identifiers[format_identifier];
def.format_identifier = format_identifier;
for (int i = 0; i < nb_meta; ++i) {
def.fields.push_back(BinField());
BinField &field = def.fields.back();
int field_id = read_double_byte(&bytes_read,&sum);
field.id = field_id;
int field_size = read_double_byte(&bytes_read,&sum);
field.size = field_size;
//printf("- field %d : %d\n", field_id, field_size);
}
int checksum = read_double_byte(&bytes_read);
//printf("- checksum %d : %d\n", checksum, sum);
if (checksum == -1) {
errors << QString("Truncated file");
return bytes_read;
} else if (checksum != sum) {
errors << QString("bad checksum: %1").arg(sum);
stop = true;
return bytes_read;
}
}
else {
int format_identifier = record_type;
//printf("- data for format identifier : %d\n", format_identifier);
if (!local_format_identifiers.contains(format_identifier)) {
errors << QString("undefined format_identifier: %1").arg(format_identifier);
stop = true;
return bytes_read;
} else {
const BinDefinition &def = local_format_identifiers[format_identifier];
//printf("- fields type : %d\n", def.format_identifier);
std::vector<int> values;
foreach(const BinField &field, def.fields) {
//printf("- field : %d \n", field.id);
int v;
switch (field.size) {
case 1: v = read_byte(&bytes_read,&sum); break;
case 2: v = read_double_byte(&bytes_read,&sum); break;
case 4: v = read_four_byte(&bytes_read,&sum); break;
case 7: v = read_date(&bytes_read,&sum); break;
default:
for (int i = 0; i < field.size; ++i)
read_byte(&bytes_read,&sum);
errors << QString("unsupported field size %1").arg(field.size);
}
values.push_back(v);
//printf("- %d : %d\n", field.id, v);
}
int checksum = read_double_byte(&bytes_read);
//printf("- checksum %d : %d\n", checksum, sum);
if (checksum == -1) {
errors << QString("Truncated file");
return bytes_read;
} else if (checksum != sum) {
errors << QString("bad checksum: %1").arg(sum);
stop = true;
return bytes_read;
}
switch (def.format_identifier) {
case RECORD_TYPE__META: decodeMetaData(def, values); break;
case RECORD_TYPE__RIDE_DATA: decodeRideData(def, values); break;
case RECORD_TYPE__RAW_DATA:
unused_record_types.insert(def.format_identifier);
break;
case RECORD_TYPE__SPARSE_DATA: decodeSparseData(def, values); break;
case RECORD_TYPE__INTERVAL_DATA: decodeIntervalData(def, values); break;
case RECORD_TYPE__DATA_ERROR: decodeDataError(def, values); break;
case RECORD_TYPE__HISTORY: decodeHistoryData(def, values); break;
default:
unexpected_record_types.insert(def.format_identifier);
}
}
}
return bytes_read;
}
RideFile * run() {
errors.clear();
rideFile = new RideFile;
rideFile->setDeviceType("Joule");
if (!file.open(QIODevice::ReadOnly)) {
delete rideFile;
return NULL;
}
//
bool stop = false;
int data_size = file.size();
int bytes_read = 0;
while (!stop && (bytes_read < data_size)) {
bytes_read += read_record(stop, errors);
}
if (last_interval_secs>0) {
rideFile->addInterval(last_interval_secs, rideFile->dataPoints().last()->secs, QString("%1").arg(interval));
}
if (stop) {
delete rideFile;
return NULL;
}
else {
foreach(int num, unknown_record_types) {
errors << QString("unknow record type %1; ignoring it").arg(num);
}
foreach(int num, unknown_format_identifiers) {
errors << QString("unknow format identifier %1; ignoring it").arg(num);
}
/*foreach(int num, unused_record_types) {
errors << QString("unused record type \"%1\" (%2)\n").arg(global_record_types[num].toAscii().constData())
.arg(num);
}
foreach(QSet<int> set, unused_format_identifiers_for_record_types) {
foreach(int num, set) {
int record_type = unused_format_identifiers_for_record_types.keys(set).takeFirst();
errors << QString("unused format identifier \"%1\" (%2) in \"%3\" (%4)\n")
.arg(global_format_identifiers[num].toAscii().constData())
.arg(num)
.arg(global_record_types[record_type].toAscii().constData())
.arg(record_type);
}
}*/
foreach(int num, unexpected_record_types) {
errors << QString("unexpected record type %1 (%2)\n").arg(global_record_types[num]).arg(num);
}
foreach(QSet<int> set, unexpected_format_identifiers_for_record_types) {
foreach(int num, set) {
int record_type = unexpected_format_identifiers_for_record_types.keys(set).takeFirst();
errors << QString("unexpected format identifier \"%1\" (%2) in \"%3\" (%4)\n")
.arg(global_format_identifiers[num].toAscii().constData())
.arg(num)
.arg(global_record_types[record_type].toAscii().constData())
.arg(record_type);
}
}
return rideFile;
}
}
};
QMap<int,QString> BinFileReaderState::global_record_types;
QMap<int,QString> BinFileReaderState::global_format_identifiers;
RideFile *BinFileReader::openRideFile(QFile &file, QStringList &errors) const
{
QSharedPointer<BinFileReaderState> state(new BinFileReaderState(file, errors));
return state->run();
}

29
src/BinRideFile.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 _BinRideFile_h
#define _BinRideFile_h
#include "RideFile.h"
struct BinFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _BinRideFile_h

66
src/ColorButton.cpp Normal file
View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "ColorButton.h"
#include <QPainter>
#include <QColorDialog>
ColorButton::ColorButton(QWidget *parent, QString name, QColor color) : QPushButton("", parent), color(color), name(name)
{
setColor(color);
connect(this, SIGNAL(clicked()), this, SLOT(clicked()));
};
void
ColorButton::setColor(QColor ncolor)
{
color = ncolor;
QPixmap pix(24, 24);
QPainter painter(&pix);
if (color.isValid()) {
painter.setPen(Qt::gray);
painter.setBrush(QBrush(color));
painter.drawRect(0, 0, 24, 24);
}
QIcon icon;
icon.addPixmap(pix);
setIcon(icon);
setContentsMargins(2,2,2,2);
setFlat(true);
setFixedWidth(34);
setMinimumWidth(34);
setMaximumWidth(34);
}
void
ColorButton::clicked()
{
// Color picker dialog
QColorDialog picker(this);
picker.setCurrentColor(color);
QColor rcolor = picker.getColor();
// if we got a good color use it and notify others
if (rcolor.isValid()) {
setColor(rcolor);
colorChosen(color);
}
}

47
src/ColorButton.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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_ColorButton_h
#define _GC_ColorButton_h 1
#include <QPushButton>
#include <QColor>
class ColorButton : public QPushButton
{
Q_OBJECT
public:
ColorButton(QWidget *parent, QString, QColor);
void setColor(QColor);
QColor getColor() { return color; }
QString getName() { return name; }
public slots:
void clicked();
signals:
void colorChosen(QColor);
protected:
QColor color;
QString name;
};
#endif

114
src/Colors.cpp Normal file
View File

@@ -0,0 +1,114 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "Colors.h"
#include "MainWindow.h"
#include <QObject>
#include <QDir>
#include "Settings.h"
static Colors *ColorList; // Initialization moved to GCColor constructor to enable translation
GCColor::GCColor(MainWindow *main) : QObject(main)
{
static Colors ColorListInit[45] = {
{ tr("Plot Background"), "COLORPLOTBACKGROUND", Qt::white },
{ tr("Plot Thumbnail Background"), "COLORPLOTTHUMBNAIL", Qt::gray },
{ tr("Plot Title"), "COLORPLOTTITLE", Qt::black },
{ tr("Plot Selection Pen"), "COLORPLOTSELECT", Qt::blue },
{ tr("Plot TrackerPen"), "COLORPLOTTRACKER", Qt::blue },
{ tr("Plot Markers"), "COLORPLOTMARKER", Qt::black },
{ tr("Plot Grid"), "COLORGRID", Qt::black },
{ tr("Interval Highlighter"), "COLORINTERVALHIGHLIGHTER", Qt::blue },
{ tr("Heart Rate"), "COLORHEARTRATE", Qt::blue },
{ tr("Speed"), "COLORSPEED", Qt::green },
{ tr("Power"), "COLORPOWER", Qt::red },
{ tr("Critical Power"), "COLORCP", Qt::red },
{ tr("Cadence"), "COLORCADENCE", QColor(0,204,204) },
{ tr("Altitude"), "COLORALTITUTDE", QColor(124,91,31) },
{ tr("Altitude Shading"), "COLORALTITUDESHADE", QColor(124,91,31) },
{ tr("Wind Speed"), "COLORWINDSPEED", Qt::darkGreen },
{ tr("Torque"), "COLORTORQUE", Qt::magenta },
{ tr("Short Term Stress"), "COLORSTS", Qt::blue },
{ tr("Long Term Stress"), "COLORLTS", Qt::green },
{ tr("Stress Balance"), "COLORSB", Qt::black },
{ tr("Daily Stress"), "COLORDAILYSTRESS", Qt::red },
{ tr("Calendar Text"), "COLORCALENDARTEXT", Qt::black },
{ tr("Power Zone 1 Shading"), "COLORZONE1", QColor(255,0,255) },
{ tr("Power Zone 2 Shading"), "COLORZONE2", QColor(42,0,255) },
{ tr("Power Zone 3 Shading"), "COLORZONE3", QColor(0,170,255) },
{ tr("Power Zone 4 Shading"), "COLORZONE4", QColor(0,255,128) },
{ tr("Power Zone 5 Shading"), "COLORZONE5", QColor(85,255,0) },
{ tr("Power Zone 6 Shading"), "COLORZONE6", QColor(255,213,0) },
{ tr("Power Zone 7 Shading"), "COLORZONE7", QColor(255,0,0) },
{ tr("Power Zone 8 Shading"), "COLORZONE8", Qt::gray },
{ tr("Power Zone 9 Shading"), "COLORZONE9", Qt::gray },
{ tr("Power Zone 10 Shading"), "COLORZONE10", Qt::gray },
{ tr("Heartrate Zone 1 Shading"), "COLORHRZONE1", QColor(255,0,255) },
{ tr("Heartrate Zone 2 Shading"), "COLORHRZONE2", QColor(42,0,255) },
{ tr("Heartrate Zone 3 Shading"), "COLORHRZONE3", QColor(0,170,255) },
{ tr("Heartrate Zone 4 Shading"), "COLORHRZONE4", QColor(0,255,128) },
{ tr("Heartrate Zone 5 Shading"), "COLORHRZONE5", QColor(85,255,0) },
{ tr("Heartrate Zone 6 Shading"), "COLORHRZONE6", QColor(255,213,0) },
{ tr("Heartrate Zone 7 Shading"), "COLORHRZONE7", QColor(255,0,0) },
{ tr("Heartrate Zone 8 Shading"), "COLORHRZONE8", Qt::gray },
{ tr("Heartrate Zone 9 Shading"), "COLORHRZONE9", Qt::gray },
{ tr("Heartrate Zone 10 Shading"), "COLORHRZONE10", Qt::gray },
{ tr("Aerolab VE"), "COLORAEROVE", Qt::blue },
{ tr("Aerolab Elevation"), "COLORAEROEL", Qt::green },
{ "", "", QColor(0,0,0) },
};
ColorList = ColorListInit;
readConfig();
connect(main, SIGNAL(configChanged()), this, SLOT(readConfig()));
}
const Colors * GCColor::colorSet()
{
return ColorList;
}
QColor
GCColor::invert(QColor color)
{
return QColor(255-color.red(), 255-color.green(), 255-color.blue());
}
void
GCColor::readConfig()
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
// read in config settings and populate the color table
for (unsigned int i=0; ColorList[i].name != ""; i++) {
QString colortext = settings->value(ColorList[i].setting, "").toString();
if (colortext != "") {
// color definitions are stored as "r:g:b"
QStringList rgb = colortext.split(":");
ColorList[i].color = QColor(rgb[0].toInt(),
rgb[1].toInt(),
rgb[2].toInt());
}
}
}
QColor
GCColor::getColor(int colornum)
{
return ColorList[colornum].color;
}

97
src/Colors.h Normal file
View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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_Colors_h
#define _GC_Colors_h 1
#include <QObject>
#include <QString>
#include <QColor>
class MainWindow;
struct Colors
{
QString name,
setting;
QColor color;
};
class GCColor : public QObject
{
Q_OBJECT
public:
GCColor(MainWindow*);
static QColor getColor(int);
static const Colors *colorSet();
static QColor invert(QColor);
public slots:
void readConfig();
};
// shorthand
#define GColor(x) GCColor::getColor(x)
#define CPLOTBACKGROUND 0
#define CPLOTTHUMBNAIL 1
#define CPLOTTITLE 2
#define CPLOTSELECT 3
#define CPLOTTRACKER 4
#define CPLOTMARKER 5
#define CPLOTGRID 6
#define CINTERVALHIGHLIGHTER 7
#define CHEARTRATE 8
#define CSPEED 9
#define CPOWER 10
#define CCP 11
#define CCADENCE 12
#define CALTITUDE 13
#define CALTITUDEBRUSH 14
#define CWINDSPEED 15
#define CTORQUE 16
#define CSTS 17
#define CLTS 18
#define CSB 19
#define CDAILYSTRESS 20
#define CCALENDARTEXT 21
#define CZONE1 22
#define CZONE2 23
#define CZONE3 24
#define CZONE4 25
#define CZONE5 26
#define CZONE6 27
#define CZONE7 28
#define CZONE8 29
#define CZONE9 30
#define CZONE10 31
#define CHZONE1 32
#define CHZONE2 33
#define CHZONE3 34
#define CHZONE4 35
#define CHZONE5 36
#define CHZONE6 37
#define CHZONE7 38
#define CHZONE8 39
#define CHZONE9 40
#define CHZONE10 41
#define CAEROVE 42
#define CAEROEL 43
#endif

View File

@@ -821,15 +821,22 @@ int Computrainer::openPort()
cfsetspeed(&deviceSettings, B2400);
// further attributes
deviceSettings.c_iflag= IGNPAR;
deviceSettings.c_oflag=0;
deviceSettings.c_iflag &= ~(IGNBRK | BRKINT | ICRNL | INLCR | PARMRK | INPCK | ICANON | ISTRIP | IXON | IXOFF | IXANY);
deviceSettings.c_iflag |= IGNPAR;
deviceSettings.c_cflag &= (~CSIZE & ~CSTOPB);
deviceSettings.c_oflag=0;
#if defined(Q_OS_MACX)
deviceSettings.c_cflag |= (CS8 | CREAD | HUPCL | CCTS_OFLOW | CRTS_IFLOW);
deviceSettings.c_cflag &= (~CCTS_OFLOW & ~CRTS_IFLOW); // no hardware flow control
deviceSettings.c_cflag |= (CS8 | CLOCAL | CREAD | HUPCL);
#else
deviceSettings.c_cflag |= (CS8 | CREAD | HUPCL | CRTSCTS);
deviceSettings.c_cflag &= (~CRTSCTS); // no hardware flow control
deviceSettings.c_cflag |= (CS8 | CLOCAL | CREAD | HUPCL);
#endif
deviceSettings.c_lflag=0;
deviceSettings.c_cc[VSTART] = 0x11;
deviceSettings.c_cc[VSTOP] = 0x13;
deviceSettings.c_cc[VEOF] = 0x20;
deviceSettings.c_cc[VMIN]=0;
deviceSettings.c_cc[VTIME]=0;
@@ -837,6 +844,7 @@ int Computrainer::openPort()
if(tcsetattr(devicePort, TCSANOW, &deviceSettings) == -1) return errno;
tcgetattr(devicePort, &deviceSettings);
tcflush(devicePort, TCIOFLUSH); // clear out the garbage
#else
// WINDOWS USES SET/GETCOMMSTATE AND READ/WRITEFILE
@@ -867,12 +875,19 @@ int Computrainer::openPort()
deviceSettings.fParity = NOPARITY;
deviceSettings.ByteSize = 8;
deviceSettings.StopBits = ONESTOPBIT;
deviceSettings.XonChar = 11;
deviceSettings.XoffChar = 13;
deviceSettings.EofChar = 0x0;
deviceSettings.ErrorChar = 0x0;
deviceSettings.EvtChar = 0x0;
deviceSettings.fBinary = true;
deviceSettings.fRtsControl = RTS_CONTROL_HANDSHAKE;
deviceSettings.fOutxCtsFlow = TRUE;
deviceSettings.fOutX = 0;
deviceSettings.fInX = 0;
deviceSettings.XonLim = 0;
deviceSettings.XoffLim = 0;
deviceSettings.fRtsControl = RTS_CONTROL_ENABLE;
deviceSettings.fDtrControl = DTR_CONTROL_ENABLE;
deviceSettings.fOutxCtsFlow = FALSE; //TRUE;
if (SetCommState(devicePort, &deviceSettings) == false) {

View File

@@ -73,10 +73,16 @@ RideFile *Computrainer3dpFileReader::openRideFile(QFile & file,
// looks like the first part is a header... ignore it.
is.skipRawData(4);
// the next 4 bytes are the ASCII characters 'Perf'
// the next 4 bytes are the ASCII characters 'perf'
char perfStr[5];
is.readRawData(perfStr, 4);
perfStr[4] = NULL;
if(strcmp(perfStr,"perf"))
{
errors << "File is encrypted.";
return NULL;
}
// not sure what the next 8 bytes are; skip them
is.skipRawData(0x8);
@@ -143,7 +149,7 @@ RideFile *Computrainer3dpFileReader::openRideFile(QFile & file,
// use that to offset distances that we report to GC so that they
// are zero-based (i.e., so that the first data point is at
// distance zero).
float firstKM;
float firstKM = 0;
bool gotFirstKM = false;
// computrainer doesn't have a fixed inter-sample-interval; GC
@@ -231,7 +237,7 @@ RideFile *Computrainer3dpFileReader::openRideFile(QFile & file,
// special case first data point
rideFile->appendPoint((double) ms/1000, (double) cad,
(double) hr, km, speed, 0.0, watts,
altitude, 0, 0, 0);
altitude, 0, 0, 0.0, 0);
}
// while loop since an interval in the .3dp file might
// span more than one CT_EMIT_MS interval
@@ -279,6 +285,7 @@ RideFile *Computrainer3dpFileReader::openRideFile(QFile & file,
interpol_alt,
0, // lon
0, // lat
0.0, // headwind
0);
// reset averaging sums

View File

@@ -20,7 +20,7 @@
#include "Computrainer.h"
#include "RealtimeData.h"
ComputrainerController::ComputrainerController(RealtimeWindow *parent, DeviceConfiguration *dc) : RealtimeController(parent)
ComputrainerController::ComputrainerController(RealtimeWindow *parent, DeviceConfiguration *dc) : RealtimeController(parent, dc)
{
myComputrainer = new Computrainer (parent, dc->portSpec);
}
@@ -81,7 +81,8 @@ ComputrainerController::getRealtimeData(RealtimeData &rtData)
msgBox.setText("Cannot Connect to Computrainer");
msgBox.setIcon(QMessageBox::Critical);
msgBox.exec();
parent->Stop();
parent->Stop(1);
return;
}
// get latest telemetry
myComputrainer->getTelemetry(Power, HeartRate, Cadence, Speed,
@@ -92,9 +93,17 @@ ComputrainerController::getRealtimeData(RealtimeData &rtData)
//
rtData.setWatts(Power);
rtData.setHr(HeartRate);
rtData.setRPM(Cadence);
rtData.setCadence(Cadence);
rtData.setSpeed(Speed);
// post processing, probably not used
// since its used to compute power for
// non-power devices, but we may add other
// calculations later that might apply
// means we could calculate power based
// upon speed even for CT!
processRealtimeData(rtData);
//
// BUTTONS
//
@@ -128,7 +137,7 @@ ComputrainerController::getRealtimeData(RealtimeData &rtData)
// if Buttons == 0 we just pressed stop!
if (Buttons&CT_RESET) {
parent->Stop();
parent->Stop(0);
}
// displaymode

View File

@@ -30,27 +30,28 @@ ConfigDialog::ConfigDialog(QDir _home, Zones *_zones, MainWindow *mainWindow) :
home = _home;
cyclistPage = new CyclistPage(zones);
cyclistPage = new CyclistPage(mainWindow);
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->setMinimumWidth(112);
contentsWidget->setMaximumWidth(112);
//contentsWidget->setMinimumHeight(200);
contentsWidget->setSpacing(12);
contentsWidget->setUniformItemSizes(true);
configPage = new ConfigurationPage();
intervalMetricsPage = new IntervalMetricsPage;
configPage = new ConfigurationPage(mainWindow);
devicePage = new DevicePage(this);
pagesWidget = new QStackedWidget;
pagesWidget->addWidget(configPage);
pagesWidget->addWidget(cyclistPage);
pagesWidget->addWidget(intervalMetricsPage);
pagesWidget->addWidget(devicePage);
#ifdef GC_HAVE_LIBOAUTH
twitterPage = new TwitterPage(this);
pagesWidget->addWidget(twitterPage);
#endif
closeButton = new QPushButton(tr("Close"));
saveButton = new QPushButton(tr("Save"));
@@ -61,10 +62,6 @@ ConfigDialog::ConfigDialog(QDir _home, Zones *_zones, MainWindow *mainWindow) :
// 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()));
// connect the pieces...
connect(devicePage->typeSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(changedType(int)));
@@ -83,41 +80,50 @@ ConfigDialog::ConfigDialog(QDir _home, Zones *_zones, MainWindow *mainWindow) :
mainLayout = new QVBoxLayout;
mainLayout->addLayout(horizontalLayout);
mainLayout->addStretch(1);
mainLayout->addSpacing(12);
//mainLayout->addStretch(1);
//mainLayout->addSpacing(12);
mainLayout->addLayout(buttonsLayout);
setLayout(mainLayout);
setWindowTitle(tr("Config Dialog"));
// We go fixed width to ensure a consistent layout for
// tabs, sub-tabs and internal widgets and lists
#ifdef Q_OS_MACX
setWindowTitle(tr("Preferences"));
#else
setWindowTitle(tr("Options"));
#endif
setFixedSize(QSize(800, 600));
}
void ConfigDialog::createIcons()
{
QListWidgetItem *configButton = new QListWidgetItem(contentsWidget);
configButton->setIcon(QIcon(":/images/config.png"));
configButton->setText(tr("Configuration"));
configButton->setText(tr("Settings"));
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->setText(tr("Athlete"));
cyclistButton->setTextAlignment(Qt::AlignHCenter);
cyclistButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
QListWidgetItem *intervalMetricsButton = new QListWidgetItem(contentsWidget);
intervalMetricsButton->setIcon(QIcon(":images/imetrics.png"));
intervalMetricsButton->setText(tr("Interval Metrics"));
intervalMetricsButton->setTextAlignment(Qt::AlignHCenter);
intervalMetricsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
QListWidgetItem *realtimeButton = new QListWidgetItem(contentsWidget);
realtimeButton->setIcon(QIcon(":images/arduino.png"));
realtimeButton->setText(tr("Devices"));
realtimeButton->setTextAlignment(Qt::AlignHCenter);
realtimeButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
#ifdef GC_HAVE_LIBOAUTH
QListWidgetItem *twitterButton = new QListWidgetItem(contentsWidget);
twitterButton->setIcon(QIcon(":images/twitter.png"));
twitterButton->setText(tr("Twitter"));
twitterButton->setTextAlignment(Qt::AlignHCenter);
twitterButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
#endif
connect(contentsWidget,
SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem *)),
this, SLOT(changePage(QListWidgetItem *, QListWidgetItem*)));
@@ -126,10 +132,6 @@ void ConfigDialog::createIcons()
}
void ConfigDialog::createNewRange()
{
}
void ConfigDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous)
{
if (!current)
@@ -151,6 +153,20 @@ void ConfigDialog::save_Clicked()
settings->setValue(GC_LANG, "fr");
else if (configPage->langCombo->currentIndex()==2)
settings->setValue(GC_LANG, "ja");
else if (configPage->langCombo->currentIndex()==3)
settings->setValue(GC_LANG, "pt-br");
else if (configPage->langCombo->currentIndex()==4)
settings->setValue(GC_LANG, "it");
else if (configPage->langCombo->currentIndex()==5)
settings->setValue(GC_LANG, "de");
else if (configPage->langCombo->currentIndex()==6)
settings->setValue(GC_LANG, "ru");
else if (configPage->langCombo->currentIndex()==7)
settings->setValue(GC_LANG, "cs");
else if (configPage->langCombo->currentIndex()==8)
settings->setValue(GC_LANG, "es");
else if (configPage->langCombo->currentIndex()==9)
settings->setValue(GC_LANG, "pt");
if (configPage->unitCombo->currentIndex()==0)
settings->setValue(GC_UNIT, "Metric");
@@ -158,6 +174,9 @@ void ConfigDialog::save_Clicked()
settings->setValue(GC_UNIT, "Imperial");
settings->setValue(GC_ALLRIDES_ASCENDING, configPage->allRidesAscending->checkState());
settings->setValue(GC_GARMIN_SMARTRECORD, configPage->garminSmartRecord->checkState());
settings->setValue(GC_GARMIN_HWMARK, configPage->garminHWMarkedit->text());
settings->setValue(GC_MAP_INTERVAL, configPage->mapIntervaledit->text());
settings->setValue(GC_CRANKLENGTH, configPage->crankLengthCombo->currentText());
settings->setValue(GC_BIKESCOREDAYS, configPage->BSdaysEdit->text());
settings->setValue(GC_BIKESCOREMODE, configPage->bsModeCombo->currentText());
@@ -166,6 +185,8 @@ void ConfigDialog::save_Clicked()
settings->setValue(GC_INITIAL_LTS, cyclistPage->perfManStart->text());
settings->setValue(GC_STS_DAYS, cyclistPage->perfManSTSavg->text());
settings->setValue(GC_LTS_DAYS, cyclistPage->perfManLTSavg->text());
settings->setValue(GC_SB_TODAY, (int) cyclistPage->showSBToday->isChecked());
settings->setValue(GC_PM_DAYS, cyclistPage->perfManDays->text());
// set default stress names if not set:
settings->setValue(GC_STS_NAME, settings->value(GC_STS_NAME,tr("Short Term Stress")));
@@ -175,34 +196,16 @@ void ConfigDialog::save_Clicked()
settings->setValue(GC_SB_NAME, settings->value(GC_SB_NAME,tr("Stress Balance")));
settings->setValue(GC_SB_ACRONYM, settings->value(GC_SB_ACRONYM,tr("SB")));
// Save Cyclist page stuff
cyclistPage->saveClicked();
// 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 = 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);
intervalMetricsPage->saveClicked();
// save interval metrics and ride data pages
configPage->saveClicked();
#ifdef GC_HAVE_LIBOAUTH
//Call Twitter Save Dialog to get Access Token
twitterPage->saveClicked();
#endif
// Save the device configuration...
DeviceConfigurations all;
all.writeConfig(devicePage->deviceListModel->Configuration);
@@ -210,70 +213,9 @@ void ConfigDialog::save_Clicked()
// Tell MainWindow we changed config, so it can emit the signal
// configChanged() to all its children
mainWindow->notifyConfigChanged();
}
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);
// close
accept();
}
//
@@ -311,6 +253,7 @@ ConfigDialog::devaddClicked()
add.type = devicePage->typeSelector->itemData(devicePage->typeSelector->currentIndex()).toInt();
add.portSpec = devicePage->deviceSpecifier->displayText();
add.deviceProfile = devicePage->deviceProfile->displayText();
add.postProcess = devicePage->virtualPower->currentIndex();
// NOT IMPLEMENTED IN THIS RELEASE XXX
//add.isDefaultDownload = devicePage->isDefaultDownload->isChecked() ? true : false;

View File

@@ -21,10 +21,6 @@ class ConfigDialog : public QDialog
public slots:
void changePage(QListWidgetItem *current, QListWidgetItem *previous);
void save_Clicked();
void back_Clicked();
void forward_Clicked();
void delete_Clicked();
void calendarDateChanged();
// device config slots
void changedType(int);
@@ -42,7 +38,7 @@ class ConfigDialog : public QDialog
ConfigurationPage *configPage;
CyclistPage *cyclistPage;
DevicePage *devicePage;
IntervalMetricsPage *intervalMetricsPage;
TwitterPage *twitterPage;
QPushButton *saveButton;
QStackedWidget *pagesWidget;
QPushButton *closeButton;

View File

@@ -17,6 +17,7 @@
*/
#include "Zones.h"
#include "Colors.h"
#include "CpintPlot.h"
#include <assert.h>
#include <unistd.h>
@@ -48,7 +49,6 @@ CpintPlot::CpintPlot(QString p, const Zones *zones) :
assert(!USE_T0_IN_CP_MODEL); // doesn't work with energyMode=true
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setCanvasBackground(Qt::white);
setAxisTitle(yLeft, tr("Average Power (watts)"));
setAxisTitle(xBottom, tr("Interval Length"));
setAxisScaleDraw(xBottom, new LogTimeScaleDraw);
@@ -57,10 +57,18 @@ CpintPlot::CpintPlot(QString p, const Zones *zones) :
grid = new QwtPlotGrid();
grid->enableX(false);
QPen gridPen;
grid->attach(this);
configChanged(); // apply colors
}
void
CpintPlot::configChanged()
{
setCanvasBackground(GColor(CPLOTBACKGROUND));
QPen gridPen(GColor(CPLOTGRID));
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
grid->attach(this);
}
struct cpi_file_info {
@@ -117,7 +125,7 @@ update_cpi_file(const cpi_file_info *info, QProgressDialog *progress,
QStringList errors;
boost::scoped_ptr<RideFile> rideFile(
RideFileFactory::instance().openRideFile(file, errors));
if (! rideFile)
if (!rideFile || rideFile->dataPoints().isEmpty())
return;
cpint_data data;
data.rec_int_ms = (int) round(rideFile->recIntSecs() * 1000.0);
@@ -126,6 +134,7 @@ update_cpi_file(const cpi_file_info *info, QProgressDialog *progress,
if (secs > 0)
data.points.append(cpint_point(secs, (int) round(p->watts)));
}
if (!data.points.count()) return;
FILE *out = fopen(info->outname.toAscii().constData(), "w");
assert(out);
@@ -441,7 +450,7 @@ CpintPlot::plot_CP_curve(CpintPlot *thisPlot, // the plot we're currently di
CPCurve = new QwtPlotCurve(curve_title);
CPCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen pen(Qt::red);
QPen pen(GColor(CCP));
pen.setWidth(2.0);
pen.setStyle(Qt::DashLine);
CPCurve->setPen(pen);
@@ -540,7 +549,7 @@ CpintPlot::plot_allCurve(CpintPlot *thisPlot,
allZoneLabels.append(label_mark);
}
high = low - 1;
high = low;
++zone;
}
}
@@ -548,10 +557,10 @@ CpintPlot::plot_allCurve(CpintPlot *thisPlot,
else {
QwtPlotCurve *curve = new QwtPlotCurve(tr("maximal power"));
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
QPen pen(Qt::red);
QPen pen(GColor(CCP));
pen.setWidth(2.0);
curve->setPen(pen);
QColor brush_color = Qt::red;
QColor brush_color = GColor(CCP);
brush_color.setAlpha(64);
curve->setBrush(brush_color); // brush fills below the line
if (energyMode_)
@@ -684,6 +693,13 @@ CpintPlot::calculate(RideItem *rideItem)
if (!needToScanRides) {
if (!CPCurve)
plot_CP_curve(this, cp, tau, t0);
else {
// make sure color reflects latest config
QPen pen(GColor(CCP));
pen.setWidth(2.0);
pen.setStyle(Qt::DashLine);
CPCurve->setPen(pen);
}
if (allCurves.empty()) {
int maxNonZero = 0;
for (int i = 0; i < bests.size(); ++i) {

View File

@@ -57,6 +57,7 @@ class CpintPlot : public QwtPlot
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);
void configChanged();
protected:

View File

@@ -26,11 +26,12 @@
#include <QFile>
#include "Season.h"
#include "SeasonParser.h"
#include "Colors.h"
#include <QXmlInputSource>
#include <QXmlSimpleReader>
CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) :
QWidget(parent), home(home), mainWindow(parent), active(false)
QWidget(parent), home(home), mainWindow(parent), currentRide(NULL)
{
QVBoxLayout *vlayout = new QVBoxLayout;
@@ -53,7 +54,7 @@ CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) :
cpintAllValue->setFixedWidth(width);
cpintCPValue->setFixedWidth(width); // so lines up nicely
cpintTimeValue->setReadOnly(true);
cpintTimeValue->setReadOnly(false);
cpintTodayValue->setReadOnly(true);
cpintAllValue->setReadOnly(true);
cpintCPValue->setReadOnly(true);
@@ -89,10 +90,12 @@ CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) :
QwtPicker::PointSelection,
QwtPicker::VLineRubberBand,
QwtPicker::AlwaysOff, cpintPlot->canvas());
picker->setRubberBandPen(QColor(Qt::blue));
picker->setRubberBandPen(GColor(CPLOTTRACKER));
connect(picker, SIGNAL(moved(const QPoint &)),
SLOT(pickerMoved(const QPoint &)));
connect(cpintTimeValue, SIGNAL(editingFinished()),
this, SLOT(cpintTimeValueEntered()));
connect(cpintSetCPButton, SIGNAL(clicked()),
this, SLOT(cpintSetCPButtonClicked()));
connect(cComboSeason, SIGNAL(currentIndexChanged(int)),
@@ -100,6 +103,10 @@ CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent) :
connect(yAxisCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(setEnergyMode(int)));
connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
connect(mainWindow, SIGNAL(configChanged()), cpintPlot, SLOT(configChanged()));
// redraw on config change -- this seems the simplest approach
connect(mainWindow, SIGNAL(configChanged()), this, SLOT(rideSelected()));
}
void
@@ -115,24 +122,17 @@ CriticalPowerWindow::deleteCpiFile(QString rideFilename)
ride_filename_to_cpi_filename(rideFilename));
}
void
CriticalPowerWindow::setActive(bool new_value)
{
bool was_active = active;
active = new_value;
if (active && !was_active) {
currentRide = mainWindow->rideItem();
if (currentRide)
cpintPlot->calculate(currentRide);
}
}
void
CriticalPowerWindow::rideSelected()
{
if (mainWindow->activeTab() != this)
return;
currentRide = mainWindow->rideItem();
if (active && currentRide) {
if (currentRide) {
cpintPlot->calculate(currentRide);
// apply latest colors
picker->setRubberBandPen(GColor(CPLOTTRACKER));
cpintSetCPButton->setEnabled(cpintPlot->cp > 0);
}
}
@@ -185,19 +185,16 @@ curve_to_point(double x, const QwtPlotCurve *curve)
}
void
CriticalPowerWindow::pickerMoved(const QPoint &pos)
CriticalPowerWindow::updateCpint(double minutes)
{
double minutes = cpintPlot->invTransform(QwtPlot::xBottom, pos.x());
cpintTimeValue->setText(interval_to_str(60.0*minutes));
// current ride
{
unsigned watts = curve_to_point(minutes, cpintPlot->getThisCurve());
QString label;
if (watts > 0)
label = QString(cpintPlot->energyMode() ? "%1 kJ" : "%1 watts").arg(watts);
label = QString(cpintPlot->energyMode() ? "%1 kJ" : "%1 watts").arg(watts);
else
label = tr("no data");
label = tr("no data");
cpintTodayValue->setText(label);
}
@@ -206,9 +203,9 @@ CriticalPowerWindow::pickerMoved(const QPoint &pos)
unsigned watts = curve_to_point(minutes, cpintPlot->getCPCurve());
QString label;
if (watts > 0)
label = QString(cpintPlot->energyMode() ? "%1 kJ" : "%1 watts").arg(watts);
label = QString(cpintPlot->energyMode() ? "%1 kJ" : "%1 watts").arg(watts);
else
label = tr("no data");
label = tr("no data");
cpintCPValue->setText(label);
}
@@ -217,7 +214,7 @@ CriticalPowerWindow::pickerMoved(const QPoint &pos)
QString label;
int index = (int) ceil(minutes * 60);
if (cpintPlot->getBests().count() > index) {
QDate date = cpintPlot->getBestDates()[index];
QDate date = cpintPlot->getBestDates()[index];
unsigned watts = cpintPlot->getBests()[index];
if (cpintPlot->energyMode())
label = QString("%1 kJ (%2)").arg(watts * minutes * 60.0 / 1000.0, 0, 'f', 0);
@@ -225,12 +222,28 @@ CriticalPowerWindow::pickerMoved(const QPoint &pos)
label = QString("%1 watts (%2)").arg(watts);
label = label.arg(date.isValid() ? date.toString(tr("MM/dd/yyyy")) : tr("no date"));
}
else
label = tr("no data");
else {
label = tr("no data");
}
cpintAllValue->setText(label);
}
}
void
CriticalPowerWindow::cpintTimeValueEntered()
{
double minutes = str_to_interval(cpintTimeValue->text()) / 60.0;
updateCpint(minutes);
}
void
CriticalPowerWindow::pickerMoved(const QPoint &pos)
{
double minutes = cpintPlot->invTransform(QwtPlot::xBottom, pos.x());
cpintTimeValue->setText(interval_to_str(60.0*minutes));
updateCpint(minutes);
}
void CriticalPowerWindow::addSeasons()
{
QFile seasonFile(home.absolutePath() + "/seasons.xml");
@@ -239,10 +252,7 @@ void CriticalPowerWindow::addSeasons()
SeasonParser( handler );
xmlReader.setContentHandler(&handler);
xmlReader.setErrorHandler(&handler);
bool ok = xmlReader.parse( source );
if(!ok)
qWarning("Failed to parse seasons.xml");
xmlReader.parse( source );
seasons = handler.getSeasons();
Season season;
season.setName(tr("All Seasons"));

View File

@@ -37,16 +37,18 @@ class CriticalPowerWindow : public QWidget
void newRideAdded();
void deleteCpiFile(QString filename);
void setActive(bool value);
protected slots:
void cpintTimeValueEntered();
void cpintSetCPButtonClicked();
void pickerMoved(const QPoint &pos);
void rideSelected();
void seasonSelected(int season);
void setEnergyMode(int index);
private:
void updateCpint(double minutes);
protected:
QDir home;
@@ -57,12 +59,11 @@ class CriticalPowerWindow : public QWidget
QLineEdit *cpintAllValue;
QLineEdit *cpintCPValue;
QComboBox *cComboSeason;
QPushButton *cpintSetCPButton;
QPushButton *cpintSetCPButton;
QwtPlotPicker *picker;
void addSeasons();
QList<Season> seasons;
RideItem *currentRide;
bool active;
};
#endif // _GC_CriticalPowerWindow_h

View File

@@ -46,6 +46,8 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
*/
QRegExp ergomoCSV("(ZEIT|STRECKE)", Qt::CaseInsensitive);
bool ergomo = false;
QChar ergomo_separator;
int unitsHeader = 1;
int total_pause = 0;
int currentInterval = 0;
@@ -75,6 +77,8 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
QTextStream is(&file);
RideFile *rideFile = new RideFile();
int iBikeInterval = 0;
bool dfpmExists = false;
int iBikeVersion = 0;
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
@@ -95,6 +99,14 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
ergomo = true;
rideFile->setDeviceType("Ergomo CSV");
unitsHeader = 2;
QStringList headers = line.split(';');
if (headers.size()>1)
ergomo_separator = ';';
else
ergomo_separator = ',';
++lineno;
continue;
}
@@ -103,6 +115,7 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
iBike = true;
rideFile->setDeviceType("iBike CSV");
unitsHeader = 5;
iBikeVersion = line.section( ',', 1, 1 ).toInt();
++lineno;
continue;
}
@@ -137,8 +150,9 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
}
}
else if (lineno > unitsHeader) {
double minutes,nm,kph,watts,km,cad,alt,hr;
double minutes,nm,kph,watts,km,cad,alt,hr,dfpm;
double lat = 0.0, lon = 0.0;
double headwind = 0.0;
int interval;
int pause;
if (!ergomo && !iBike) {
@@ -162,10 +176,21 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
// 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
//
// For iBike software version 11 or higher:
// use "power" field until a the "dfpm" field becomes non-zero.
minutes = (recInterval * lineno - unitsHeader)/60.0;
nm = NULL; //no torque
kph = line.section(',', 0, 0).toDouble();
watts = line.section(',', 2, 2).toDouble();
dfpm = line.section( ',', 11, 11).toDouble();
if( iBikeVersion >= 11 && ( dfpm > 0.0 || dfpmExists ) ) {
dfpmExists = true;
watts = dfpm;
headwind = line.section(',', 1, 1).toDouble();
}
else {
watts = line.section(',', 2, 2).toDouble();
}
km = line.section(',', 3, 3).toDouble();
cad = line.section(',', 4, 4).toDouble();
hr = line.section(',', 5, 5).toDouble();
@@ -181,24 +206,29 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
km *= KM_PER_MILE;
kph *= KM_PER_MILE;
alt *= METERS_PER_FOOT;
headwind *= KM_PER_MILE;
}
}
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();
minutes = line.section(ergomo_separator, 0, 0).toDouble() + total_pause;
QString km_string = line.section(ergomo_separator, 1, 1);
km_string.replace(",",".");
km = km_string.toDouble();
watts = line.section(ergomo_separator, 2, 2).toDouble();
cad = line.section(ergomo_separator, 3, 3).toDouble();
QString kph_string = line.section(ergomo_separator, 4, 4);
kph_string.replace(",",".");
kph = kph_string.toDouble();
hr = line.section(ergomo_separator, 5, 5).toDouble();
alt = line.section(ergomo_separator, 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();
pause = line.section(ergomo_separator, 9, 9).toInt();
total_pause += pause;
nm = NULL; // torque is not provided in the Ergomo file
@@ -220,7 +250,7 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
watts = 0;
rideFile->appendPoint(minutes * 60.0, cad, hr, km,
kph, nm, watts, alt, lat, lon, interval);
kph, nm, watts, alt, lon, lat, headwind, interval);
}
++lineno;
}
@@ -254,10 +284,15 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$");
rideTime.setCaseSensitivity(Qt::CaseInsensitive);
if (startTime != QDateTime()) {
// Start time was already set above?
rideFile->setStartTime(startTime);
}
else if (rideTime.indexIn(file.fileName()) >= 0) {
} else if (rideTime.indexIn(file.fileName()) >= 0) {
// It matches the GC naming convention?
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
rideTime.cap(2).toInt(),
rideTime.cap(3).toInt()),
@@ -265,7 +300,9 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
rideTime.cap(5).toInt(),
rideTime.cap(6).toInt()));
rideFile->setStartTime(datetime);
} else {
// Could be yyyyddmm_hhmmss_NAME.csv (case insensitive)
rideTime.setPattern("(\\d\\d\\d\\d)(\\d\\d)(\\d\\d)_(\\d\\d)(\\d\\d)(\\d\\d)[^\\.]*\\.csv$");
if (rideTime.indexIn(file.fileName()) >= 0) {
@@ -277,9 +314,39 @@ RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
rideTime.cap(6).toInt()));
rideFile->setStartTime(datetime);
} else {
qWarning("Failed to set start time");
// is it in poweragent format "name yyyy-mm-dd hh-mm-ss.csv"
rideTime.setPattern("(\\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);
} else {
// NO DICE
// XXX Note: qWarning("Failed to set start time");
// console messages are no use, so commented out
// this problem will ONLY occur during the import
// process which traps these and corrects them
// so no need to do anything here
}
}
}
return rideFile;
}
// did we actually read any samples?
if (rideFile->dataPoints().count() > 0) {
return rideFile;
} else {
errors << "No samples present.";
delete rideFile;
return NULL;
}
}

View File

@@ -28,147 +28,296 @@
#include <assert.h>
#include <math.h>
#include <QtXml/QtXml>
#include <QFile>
#include <QFileInfo>
#include "SummaryMetrics.h"
#include "RideMetadata.h"
#include "SpecialFields.h"
#include <boost/scoped_array.hpp>
#include <boost/crc.hpp>
DBAccess::DBAccess(QDir home)
// DB Schema Version - YOU MUST UPDATE THIS IF THE SCHEMA VERSION CHANGES!!!
// Schema version will change if a) the default metadata.xml is updated
// or b) new metrics are added / old changed
static int DBSchemaVersion = 17;
DBAccess::DBAccess(MainWindow* main, QDir home) : main(main), home(home)
{
initDatabase(home);
}
void DBAccess::closeConnection()
{
db.close();
dbconn.close();
}
QSqlDatabase DBAccess::initDatabase(QDir home)
DBAccess::~DBAccess()
{
closeConnection();
}
void
DBAccess::initDatabase(QDir home)
{
if(db.isOpen())
return db;
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(home.absolutePath() + "/metricDB");
if (!db.open()) {
if(dbconn.isOpen()) return;
QString cyclist = QFileInfo(home.path()).baseName();
sessionid = QString("%1%2").arg(cyclist).arg(main->session++);
if (main->session == 1) {
// first
main->db = QSqlDatabase::addDatabase("QSQLITE", sessionid);
main->db.setDatabaseName(home.absolutePath() + "/metricDB");
//dbconn = db.database(QString("GC"));
dbconn = main->db.database(sessionid);
} else {
// clone the first one!
dbconn = QSqlDatabase::cloneDatabase(main->db, sessionid);
dbconn.open();
}
if (!dbconn.isOpen()) {
QMessageBox::critical(0, qApp->translate("DBAccess","Cannot open database"),
qApp->translate("DBAccess","Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"This feature requires 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;
} else {
// create database - does nothing if its already there
createDatabase();
}
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();
SpecialFields sp;
QSqlQuery query(dbconn);
bool rc;
bool createTables = true;
// does the table exist?
rc = query.exec("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
if (rc) {
while (query.next()) {
QString table = query.value(0).toString();
if (table == "metrics") {
createTables = false;
break;
}
}
}
// we need to create it!
if (rc && createTables) {
QString createMetricTable = "create table metrics (filename varchar primary key,"
"timestamp integer,"
"ride_date date,"
"fingerprint integer";
// Add columns for all the metric factory metrics
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
createMetricTable += QString(", X%1 double").arg(factory.metricName(i));
// And all the metadata metrics
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
createMetricTable += QString(", Z%1 double").arg(sp.makeTechName(field.name));
}
}
createMetricTable += " )";
rc = query.exec(createMetricTable);
if (!rc) {
qDebug()<<"create table failed!" << query.lastError();
}
}
return rc;
}
bool DBAccess::dropMetricTable()
{
QSqlQuery query("DROP TABLE metrics", dbconn);
return query.exec();
}
bool DBAccess::createDatabase()
{
// check schema version and if missing recreate database
checkDBVersion();
bool rc = false;
rc = createMetricsTable();
if(!rc)
return rc;
rc = createIndex();
if(!rc)
return rc;
// at present only one table!
bool rc = createMetricsTable();
if(!rc) return rc;
// other tables here
return true;
}
bool DBAccess::createIndex()
static int
computeFileCRC(QString filename)
{
QSqlQuery query;
query.prepare("create INDEX IDX_FILENAME on metrics(filename)");
bool rc = query.exec();
if(!rc)
qDebug() << query.lastError();
return rc;
QFile file(filename);
QFileInfo fileinfo(file);
// open file
if (!file.open(QFile::ReadOnly)) return 0;
// allocate space
boost::scoped_array<char> data(new char[file.size()]);
// read entire file into memory
QDataStream *rawstream(new QDataStream(&file));
rawstream->readRawData(&data[0], file.size());
file.close();
// calculate the CRC
boost::crc_optimal<16, 0x1021, 0xFFFF, 0, false, false> CRC;
CRC.process_bytes(&data[0], file.size());
return CRC.checksum();
}
bool DBAccess::importRide(SummaryMetrics *summaryMetrics )
void DBAccess::checkDBVersion()
{
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 (?,?,?,?,?,?,?,?,?,?,?,?,?)");
int currentversion = 0;
int metadatacrc; // crc for metadata.xml when last refreshed
int metadatacrcnow; // current value for metadata.xml crc
int creationdate;
// get a CRC for metadata.xml
QString metadataXML = QString(home.absolutePath()) + "/metadata.xml";
metadatacrcnow = computeFileCRC(metadataXML);
// can we get a version number?
QSqlQuery query("SELECT schema_version, creation_date, metadata_crc from version;", dbconn);
bool rc = query.exec();
while (rc && query.next()) {
currentversion = query.value(0).toInt();
creationdate = query.value(1).toInt();
metadatacrc = query.value(2).toInt();
}
// if its not up-to-date
if (!rc || currentversion != DBSchemaVersion || metadatacrc != metadatacrcnow) {
// drop tables
QSqlQuery dropV("DROP TABLE version", dbconn);
dropV.exec();
QSqlQuery dropM("DROP TABLE metrics", dbconn);
dropM.exec();
// recreate version table and add one entry
QSqlQuery version("CREATE TABLE version ( schema_version integer primary key, creation_date date, metadata_crc integer );", dbconn);
version.exec();
// insert current version number
QDateTime timestamp = QDateTime::currentDateTime();
QSqlQuery insert("INSERT INTO version ( schema_version, creation_date, metadata_crc ) values (?,?,?)", dbconn);
insert.addBindValue(DBSchemaVersion);
insert.addBindValue(timestamp.toTime_t());
insert.addBindValue(metadatacrcnow);
insert.exec();
createMetricsTable();
}
}
/*----------------------------------------------------------------------
* CRUD routines for Metrics table
*----------------------------------------------------------------------*/
bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, unsigned long fingerprint, bool modify)
{
SpecialFields sp;
QSqlQuery query(dbconn);
QDateTime timestamp = QDateTime::currentDateTime();
if (modify) {
// zap the current row
query.prepare("DELETE FROM metrics WHERE filename = ?;");
query.addBindValue(summaryMetrics->getFileName());
query.exec();
}
// construct an insert statement
QString insertStatement = "insert into metrics ( filename, timestamp, ride_date, fingerprint ";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
insertStatement += QString(", X%1 ").arg(factory.metricName(i));
// And all the metadata metrics
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
insertStatement += QString(", Z%1 ").arg(sp.makeTechName(field.name));
}
}
insertStatement += " ) values (?,?,?,?"; // filename, timestamp, ride_date
for (int i=0; i<factory.metricCount(); i++)
insertStatement += ",?";
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
insertStatement += ",?";
}
}
insertStatement += ")";
query.prepare(insertStatement);
// filename, timestamp, ride date
query.addBindValue(summaryMetrics->getFileName());
query.addBindValue(timestamp.toTime_t());
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();
query.addBindValue((int)fingerprint);
// values
for (int i=0; i<factory.metricCount(); i++) {
query.addBindValue(summaryMetrics->getForSymbol(factory.metricName(i)));
}
// And all the metadata metrics
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
query.addBindValue(ride->getTag(field.name, "0.0").toDouble());
} else if (!sp.isMetric(field.name)) {
if (field.name == "Recording Interval") // XXX Special - need a better way...
query.addBindValue(ride->recIntSecs());
}
}
// go do it!
bool rc = query.exec();
if(!rc)
{
qDebug() << query.lastError();
}
return rc;
}
QStringList DBAccess::getAllFileNames()
bool
DBAccess::deleteRide(QString name)
{
QSqlQuery query("SELECT filename from metrics");
QStringList fileList;
while(query.next())
{
QString filename = query.value(0).toString();
fileList << filename;
}
return fileList;
QSqlQuery query(dbconn);
query.prepare("DELETE FROM metrics WHERE filename = ?;");
query.addBindValue(name);
return query.exec();
}
QList<QDateTime> DBAccess::getAllDates()
{
QSqlQuery query("SELECT ride_date from metrics");
QSqlQuery query("SELECT ride_date from metrics ORDER BY ride_date;", dbconn);
QList<QDateTime> dates;
query.exec();
while(query.next())
{
QDateTime date = query.value(0).toDateTime();
@@ -179,48 +328,52 @@ QList<QDateTime> DBAccess::getAllDates()
QList<SummaryMetrics> DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
{
SpecialFields sp;
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);
// null date range fetches all, but not currently used by application code
// since it relies too heavily on the results of the QDateTime constructor
if (start == QDateTime()) start = QDateTime::currentDateTime().addYears(-10);
if (end == QDateTime()) end = QDateTime::currentDateTime().addYears(+10);
// construct the select statement
QString selectStatement = "SELECT filename, ride_date";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++)
selectStatement += QString(", X%1 ").arg(factory.metricName(i));
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
selectStatement += QString(", Z%1 ").arg(sp.makeTechName(field.name));
}
}
selectStatement += " FROM metrics where DATE(ride_date) >=DATE(:start) AND DATE(ride_date) <=DATE(:end) "
" ORDER BY ride_date;";
// execute the select statement
QSqlQuery query(selectStatement, dbconn);
query.bindValue(":start", start.date());
query.bindValue(":end", end.date());
query.exec();
while(query.next())
{
SummaryMetrics summaryMetrics;
// filename and date
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());
// the values
int i=0;
for (; i<factory.metricCount(); i++)
summaryMetrics.setForSymbol(factory.metricName(i), query.value(i+2).toDouble());
foreach(FieldDefinition field, main->rideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
QString underscored = field.name;
summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+2).toDouble());
i++;
}
}
metrics << summaryMetrics;
}
return metrics;
}
bool DBAccess::dropMetricTable()
{
QStringList tableList = db.tables(QSql::Tables);
if(!tableList.contains("metrics"))
return true;
QSqlQuery query("DROP TABLE metrics");
return query.exec();
}

View File

@@ -25,7 +25,9 @@
#include <QHash>
#include <QtSql>
#include "SummaryMetrics.h"
#include "MainWindow.h"
#include "Season.h"
#include "RideFile.h"
class RideFile;
class Zones;
@@ -34,23 +36,41 @@ 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();
// get connection name
QSqlDatabase connection() { return dbconn; }
// check the db structure is up to date
void checkDBVersion();
// create and drop connections
DBAccess(MainWindow *main, QDir home);
~DBAccess();
// Create/Delete Records
bool importRide(SummaryMetrics *summaryMetrics, RideFile *ride, unsigned long, bool);
bool deleteRide(QString);
// Query Records
QList<QDateTime> getAllDates();
QList<SummaryMetrics> getAllMetricsFor(QDateTime start, QDateTime end);
bool createMetricsTable();
QList<Season> getAllSeasons();
bool dropMetricTable();
private:
QSqlDatabase db;
MainWindow *main;
QDir home;
QSqlDatabase dbconn;
QString sessionid;
typedef QHash<QString,RideMetric*> MetricMap;
bool createDatabase();
void closeConnection();
bool createMetricsTable();
bool dropMetricTable();
bool createIndex();
QSqlDatabase initDatabase(QDir home);
void initDatabase(QDir home);
};
#endif
#endif

View File

@@ -18,7 +18,9 @@
#include "RideMetric.h"
#include "Zones.h"
#include <QObject>
#include <math.h>
#include <QApplication>
// The idea: Fit a curve to the points system in Table 2.2 of "Daniel's Running
// Formula", Second Edition, assume that power at VO2Max is 1.2 * FTP, further
@@ -30,38 +32,34 @@
class DanielsPoints : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(DanielsPoints)
static const double K;
double score;
void count(double secs, double watts, double cp) {
void inc(double secs, double watts, double cp) {
score += K * secs * pow(watts / cp, 4);
}
public:
DanielsPoints() : score(0.0) {}
QString symbol() const { return "daniels_points"; }
QString name() const { return QObject::tr("Daniels Points"); }
QString units(bool) const { return ""; }
int precision() const { return 0; }
double value(bool) const { return score; }
void compute(const RideFile *ride, const Zones *zones,
int zoneRange, const QHash<QString,RideMetric*> &) {
if (!zones || zoneRange < 0)
return;
static const double K;
if (ride->deviceType() == QString("Manual CSV")) {
// Manual entry: use BS from dataPoints with a scaling factor
// that works about right for long, steady rides.
double scaling_factor = 0.55;
if (ride->metricOverrides.contains("skiba_bike_score")) {
const QMap<QString,QString> bsm =
ride->metricOverrides.value("skiba_bike_score");
if (bsm.contains("value")) {
double bs = bsm.value("value").toDouble();
score = bs * scaling_factor;
}
}
DanielsPoints() : score(0.0)
{
setSymbol("daniels_points");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Daniels Points");
}
void initialize() {
#endif
setName(tr("Daniels Points"));
setMetricUnits("");
setImperialUnits("");
setType(RideMetric::Total);
}
void compute(const RideFile *ride, const Zones *zones,
int zoneRange, const HrZones *, int, const QHash<QString,RideMetric*> &) {
if (!zones || zoneRange < 0) {
setValue(0);
return;
}
@@ -84,34 +82,77 @@ class DanielsPoints : public RideMetric {
&& (point->secs > lastSecs + secsDelta + EPSILON)) {
weighted *= attenuation;
lastSecs += secsDelta;
count(secsDelta, weighted, cp);
inc(secsDelta, weighted, cp);
}
weighted *= attenuation;
weighted += sampleWeight * point->watts;
lastSecs = point->secs;
count(secsDelta, weighted, cp);
inc(secsDelta, weighted, cp);
}
while (weighted > NEGLIGIBLE) {
weighted *= attenuation;
lastSecs += secsDelta;
count(secsDelta, weighted, cp);
inc(secsDelta, weighted, cp);
}
setValue(score);
}
void override(const QMap<QString,QString> &map) {
if (map.contains("value"))
score = map.value("value").toDouble();
}
void aggregateWith(const RideMetric &other) { score += other.value(true); }
RideMetric *clone() const { return new DanielsPoints(*this); }
};
// Choose K such that 1 hour at FTP yields a score of 100.
const double DanielsPoints::K = 100.0 / 3600.0;
class DanielsEquivalentPower : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(DanielsEquivalentPower)
double watts;
public:
DanielsEquivalentPower() : watts(0.0)
{
setSymbol("daniels_equivalent_power");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("Daniels EqP");
}
void initialize() {
#endif
setName(tr("Daniels EqP"));
setMetricUnits(tr("watts"));
setImperialUnits(tr("watts"));
setType(RideMetric::Average);
}
void compute(const RideFile *, const Zones *zones, int zoneRange, const HrZones *, int,
const QHash<QString,RideMetric*> &deps)
{
if (!zones || zoneRange < 0) {
setValue(0);
return;
}
double cp = zones->getCP(zoneRange);
assert(deps.contains("daniels_points"));
assert(deps.contains("time_riding"));
const RideMetric *danielsPoints = deps.value("daniels_points");
const RideMetric *timeRiding = deps.value("time_riding");
assert(danielsPoints);
assert(timeRiding);
double score = danielsPoints->value(true);
double secs = timeRiding->value(true);
watts = secs == 0.0 ? 0.0 : cp * pow(score / DanielsPoints::K / secs, 0.25);
setValue(watts);
}
RideMetric *clone() const { return new DanielsEquivalentPower(*this); }
};
static bool added() {
RideMetricFactory::instance().addMetric(DanielsPoints());
QVector<QString> deps;
deps.append("time_riding");
deps.append("daniels_points");
RideMetricFactory::instance().addMetric(DanielsEquivalentPower(), &deps);
return true;
}
static bool added_ = added();

121
src/DataProcessor.cpp Normal file
View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2010 mark Liversedge )liversedge@gmail.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 "DataProcessor.h"
#include "MainWindow.h"
#include "AllPlot.h"
#include "Settings.h"
#include "Units.h"
#include <assert.h>
DataProcessorFactory *DataProcessorFactory::instance_;
DataProcessorFactory &DataProcessorFactory::instance()
{
if (!instance_) instance_ = new DataProcessorFactory();
return *instance_;
}
bool
DataProcessorFactory::registerProcessor(QString name, DataProcessor *processor)
{
assert(!processors.contains(name));
processors.insert(name, processor);
return true;
}
bool
DataProcessorFactory::autoProcess(RideFile *ride)
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
bool changed = false;
// run through the processors and execute them!
QMapIterator<QString, DataProcessor*> i(processors);
i.toFront();
while (i.hasNext()) {
i.next();
QString configsetting = QString("dp/%1/apply").arg(i.key());
if (settings->value(configsetting, "Manual").toString() == "Auto")
i.value()->postProcess(ride);
}
return changed;
}
ManualDataProcessorDialog::ManualDataProcessorDialog(MainWindow *main, QString name, RideItem *ride) : main(main), ride(ride)
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(name);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// find our processor
const DataProcessorFactory &factory = DataProcessorFactory::instance();
QMap<QString, DataProcessor*> processors = factory.getProcessors();
processor = processors.value(name, NULL);
if (processor == NULL) reject();
// Change window title to Localized Name
setWindowTitle(processor->name());
QFont font;
font.setWeight(QFont::Black);
QLabel *configLabel = new QLabel(tr("Settings"), this);
configLabel->setFont(font);
QLabel *explainLabel = new QLabel(tr("Description"), this);
explainLabel->setFont(font);
config = processor->processorConfig(this);
config->readConfig();
explain = new QTextEdit(this);
explain->setText(config->explain());
explain->setReadOnly(true);
mainLayout->addWidget(configLabel);
mainLayout->addWidget(config);
mainLayout->addWidget(explainLabel);
mainLayout->addWidget(explain);
ok = new QPushButton(tr("OK"), this);
cancel = new QPushButton(tr("Cancel"), this);
QHBoxLayout *buttons = new QHBoxLayout();
buttons->addStretch();
buttons->addWidget(cancel);
buttons->addWidget(ok);
mainLayout->addLayout(buttons);
connect(ok, SIGNAL(clicked()), this, SLOT(okClicked()));
connect(cancel, SIGNAL(clicked()), this, SLOT(cancelClicked()));
}
void
ManualDataProcessorDialog::okClicked()
{
if (processor->postProcess((RideFile *)ride->ride(), config) == true) {
main->notifyRideSelected(); // XXX to remain compatible with rest of GC for now
}
accept();
}
void
ManualDataProcessorDialog::cancelClicked()
{
reject();
}

115
src/DataProcessor.h Normal file
View File

@@ -0,0 +1,115 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 _DataProcessor_h
#define _DataProcessor_h
#include "RideFile.h"
#include "RideFileCommand.h"
#include "RideItem.h"
#include <QDate>
#include <QDir>
#include <QFile>
#include <QList>
#include <QMap>
#include <QVector>
// This file defines four classes:
//
// DataProcessorConfig is a base QWidget that must be supplied by the
// DataProcessor to enable the user to configure its options
//
// DataProcessor is an abstract base class for function-objects that take a
// rideFile and manipulate it. Examples include fixing gaps in recording or
// creating the .notes or .cpi file
//
// DataProcessorFactory is a singleton that maintains a mapping of
// all DataProcessor objects that can be applied to rideFiles
//
// ManualDataProcessorDialog is a dialog box to manually execute a
// dataprocessor on the current ride and is called from the mainWindow menus
//
#include <QtGui>
// every data processor must supply a configuration Widget
// when its processorConfig member is called
class DataProcessorConfig : public QWidget
{
Q_OBJECT
public:
DataProcessorConfig(QWidget *parent=0) : QWidget(parent) {}
virtual ~DataProcessorConfig() {}
virtual void readConfig() = 0;
virtual void saveConfig() = 0;
virtual QString explain() = 0;
};
// the data processor abstract base class
class DataProcessor
{
public:
DataProcessor() {}
virtual ~DataProcessor() {}
virtual bool postProcess(RideFile *, DataProcessorConfig*settings=0) = 0;
virtual DataProcessorConfig *processorConfig(QWidget *parent) = 0;
virtual QString name() = 0; // Localized Name for user interface
};
// all data processors
class DataProcessorFactory {
private:
static DataProcessorFactory *instance_;
QMap<QString,DataProcessor*> processors;
DataProcessorFactory() {}
public:
static DataProcessorFactory &instance();
bool registerProcessor(QString name, DataProcessor *processor);
QMap<QString,DataProcessor*> getProcessors() const { return processors; }
bool autoProcess(RideFile *); // run auto processes (after open rideFile)
};
class MainWindow;
class ManualDataProcessorDialog : public QDialog
{
Q_OBJECT
public:
ManualDataProcessorDialog(MainWindow *, QString, RideItem *);
private slots:
void cancelClicked();
void okClicked();
private:
MainWindow *main;
RideItem *ride;
DataProcessor *processor;
DataProcessorConfig *config;
QTextEdit *explain;
QPushButton *ok, *cancel;
};
#endif // _DataProcessor_h

View File

@@ -21,10 +21,13 @@
* Provides specialized formatting for Plot axes
*/
#include <QApplication>
#include <qwt_scale_draw.h>
class DaysScaleDraw: public QwtScaleDraw
{
Q_DECLARE_TR_FUNCTIONS(DaysScaleDraw)
public:
DaysScaleDraw()
{
@@ -34,25 +37,25 @@ class DaysScaleDraw: public QwtScaleDraw
switch(int(v))
{
case 1:
return QString("Mon");
return QString(tr("Mon"));
break;
case 2:
return QString("Tue");
return QString(tr("Tue"));
break;
case 3:
return QString("Wed");
return QString(tr("Wed"));
break;
case 4:
return QString("Thu");
return QString(tr("Thu"));
break;
case 5:
return QString("Fri");
return QString(tr("Fri"));
break;
case 6:
return QString("Sat");
return QString(tr("Sat"));
break;
case 7:
return QString("Sun");
return QString(tr("Sun"));
break;
default:
return QString(int(v));

View File

@@ -33,6 +33,7 @@ DeviceConfiguration::DeviceConfiguration()
type=0;
isDefaultDownload=false;
isDefaultRealtime=false;
postProcess=0;
}
@@ -98,6 +99,10 @@ DeviceConfigurations::readConfig()
configVal = settings->value(configStr);
Entry.isDefaultRealtime = configVal.toInt();
configStr = QString("%1%2").arg(GC_DEV_VIRTUAL).arg(i+1);
configVal = settings->value(configStr);
Entry.postProcess = configVal.toInt();
Entries.append(Entry);
}
return Entries;
@@ -138,6 +143,10 @@ DeviceConfigurations::writeConfig(QList<DeviceConfiguration> Configuration)
// isDefaultRealtime
configStr = QString("%1%2").arg(GC_DEV_DEFR).arg(i+1);
settings->setValue(configStr, Configuration.at(i).isDefaultRealtime);
// virtual post Process...
configStr = QString("%1%2").arg(GC_DEV_VIRTUAL).arg(i+1);
settings->setValue(configStr, Configuration.at(i).postProcess);
}
}

View File

@@ -36,6 +36,8 @@ class DeviceConfiguration
bool isDefaultDownload, // not implemented yet
isDefaultRealtime; // not implemented yet
int postProcess;
};
class DeviceConfigurations

View File

@@ -33,7 +33,7 @@ DownloadRideDialog::DownloadRideDialog(MainWindow *mainWindow,
downloadInProgress(false)
{
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle("Download Ride Data");
setWindowTitle(tr("Download Ride Data"));
portCombo = new QComboBox(this);
@@ -57,6 +57,7 @@ DownloadRideDialog::DownloadRideDialog(MainWindow *mainWindow,
connect(eraseRideButton, SIGNAL(clicked()), this, SLOT(eraseClicked()));
connect(rescanButton, SIGNAL(clicked()), this, SLOT(scanCommPorts()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
connect(deviceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setReadyInstruct()));
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(downloadButton);
@@ -90,9 +91,9 @@ DownloadRideDialog::setReadyInstruct()
Device &device = Device::device(deviceCombo->currentText());
QString inst = device.downloadInstructions();
if (inst.size() == 0)
label->setText("Click Download to begin downloading.");
label->setText(tr("Click Download to begin downloading."));
else
label->setText(inst + ", \nthen click Download.");
label->setText(inst + tr(", \nthen click Download."));
downloadButton->setEnabled(true);
if (deviceCombo->currentText() == "SRM") // only SRM supports erase ride for now
eraseRideButton->setEnabled(true);
@@ -106,9 +107,9 @@ DownloadRideDialog::scanCommPorts()
QString err;
devList = CommPort::listCommPorts(err);
if (err != "") {
QString msg = "Warning(s):\n\n" + err + "\n\nYou may need to (re)install "
"the FTDI or PL2303 drivers before downloading.";
QMessageBox::warning(0, "Error Loading Device Drivers", msg,
QString msg = tr("Warning(s):\n\n") + err + tr("\n\nYou may need to (re)install "
"the FTDI or PL2303 drivers before downloading.");
QMessageBox::warning(0, tr("Error Loading Device Drivers"), msg,
QMessageBox::Ok, QMessageBox::NoButton);
}
for (int i = 0; i < devList.size(); ++i) {

View File

@@ -99,7 +99,7 @@ ErgFile::ErgFile(QString filename, int &mode, double Cp)
mode = format = ERG;
} else if (mrcformat.exactMatch(line)) {
// save away the format
mode = format = ERG;
mode = format = MRC;
} else if (crsformat.exactMatch(line)) {
// save away the format
mode = format = CRS;

624
src/FitRideFile.cpp Normal file
View File

@@ -0,0 +1,624 @@
/*
* 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 "FitRideFile.h"
#include <QSharedPointer>
#include <QMap>
#include <QSet>
#include <QtEndian>
#include <QDebug>
#include <stdio.h>
#include <stdint.h>
#include <limits>
#define RECORD_TYPE 20
static int fitFileReaderRegistered =
RideFileFactory::instance().registerReader(
"fit", "Garmin FIT", new FitFileReader());
static const QDateTime qbase_time(QDate(1989, 12, 31), QTime(0, 0, 0), Qt::UTC);
struct FitField {
int num;
int type; // FIT base_type
int size; // in bytes
};
struct FitDefinition {
int global_msg_num;
bool is_big_endian;
std::vector<FitField> fields;
};
/* FIT has uint32 as largest integer type. So qint64 is large enough to
* store all integer types - no matter if they're signed or not */
// XXX this needs to get changed to support non-integer values
typedef qint64 fit_value_t;
#define NA_VALUE std::numeric_limits<fit_value_t>::max()
struct FitFileReaderState
{
QFile &file;
QStringList &errors;
RideFile *rideFile;
time_t start_time;
time_t last_time;
double last_distance;
QMap<int, FitDefinition> local_msg_types;
QSet<int> unknown_record_fields, unknown_global_msg_nums, unknown_base_type;
int interval;
int devices;
bool stopped;
int last_event_type;
int last_event;
int last_msg_type;
FitFileReaderState(QFile &file, QStringList &errors) :
file(file), errors(errors), rideFile(NULL), start_time(0),
last_time(0), last_distance(0.00f), interval(0), devices(0), stopped(true),
last_event_type(-1), last_event(-1), last_msg_type(-1)
{
}
struct TruncatedRead {};
struct BadDelta {};
void read_unknown( int size, int *count = NULL ){
char c[size+1];
// XXX: just seek instead of read?
if (file.read(c, size ) != size)
throw TruncatedRead();
if (count)
(*count) += size;
}
fit_value_t read_int8(int *count = NULL) {
qint8 i;
if (file.read(reinterpret_cast<char*>( &i), 1) != 1)
throw TruncatedRead();
if (count)
(*count) += 1;
return i == 0x7f ? NA_VALUE : i;
}
fit_value_t read_uint8(int *count = NULL) {
quint8 i;
if (file.read(reinterpret_cast<char*>( &i), 1) != 1)
throw TruncatedRead();
if (count)
(*count) += 1;
return i == 0xff ? NA_VALUE : i;
}
fit_value_t read_uint8z(int *count = NULL) {
quint8 i;
if (file.read(reinterpret_cast<char*>( &i), 1) != 1)
throw TruncatedRead();
if (count)
(*count) += 1;
return i == 0x00 ? NA_VALUE : i;
}
fit_value_t read_int16(bool is_big_endian, int *count = NULL) {
qint16 i;
if (file.read(reinterpret_cast<char*>(&i), 2) != 2)
throw TruncatedRead();
if (count)
(*count) += 2;
i = is_big_endian
? qFromBigEndian<qint16>( i )
: qFromLittleEndian<qint16>( i );
return i == 0x7fff ? NA_VALUE : i;
}
fit_value_t read_uint16(bool is_big_endian, int *count = NULL) {
quint16 i;
if (file.read(reinterpret_cast<char*>(&i), 2) != 2)
throw TruncatedRead();
if (count)
(*count) += 2;
i = is_big_endian
? qFromBigEndian<quint16>( i )
: qFromLittleEndian<quint16>( i );
return i == 0xffff ? NA_VALUE : i;
}
fit_value_t read_uint16z(bool is_big_endian, int *count = NULL) {
quint16 i;
if (file.read(reinterpret_cast<char*>(&i), 2) != 2)
throw TruncatedRead();
if (count)
(*count) += 2;
i = is_big_endian
? qFromBigEndian<quint16>( i )
: qFromLittleEndian<quint16>( i );
return i == 0x0000 ? NA_VALUE : i;
}
fit_value_t read_int32(bool is_big_endian, int *count = NULL) {
qint32 i;
if (file.read(reinterpret_cast<char*>(&i), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
i = is_big_endian
? qFromBigEndian<qint32>( i )
: qFromLittleEndian<qint32>( i );
return i == 0x7fffffff ? NA_VALUE : i;
}
fit_value_t read_uint32(bool is_big_endian, int *count = NULL) {
quint32 i;
if (file.read(reinterpret_cast<char*>(&i), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
i = is_big_endian
? qFromBigEndian<quint32>( i )
: qFromLittleEndian<quint32>( i );
return i == 0xffffffff ? NA_VALUE : i;
}
fit_value_t read_uint32z(bool is_big_endian, int *count = NULL) {
quint32 i;
if (file.read(reinterpret_cast<char*>(&i), 4) != 4)
throw TruncatedRead();
if (count)
(*count) += 4;
i = is_big_endian
? qFromBigEndian<quint32>( i )
: qFromLittleEndian<quint32>( i );
return i == 0x00000000 ? NA_VALUE : i;
}
void decodeFileId(const FitDefinition &def, int, const std::vector<fit_value_t> values) {
int i = 0;
int manu = -1, prod = -1;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++];
if( value == NA_VALUE )
continue;
switch (field.num) {
case 1: manu = value; break;
case 2: prod = value; break;
default: ; // do nothing
}
}
if (manu == 1) {
switch (prod) {
case 717: rideFile->setDeviceType("Garmin FR405"); break;
case 782: rideFile->setDeviceType("Garmin FR50"); break;
case 988: rideFile->setDeviceType("Garmin FR60"); break;
case 1018: rideFile->setDeviceType("Garmin FR310XT"); break;
case 1036: rideFile->setDeviceType("Garmin Edge 500"); break;
case 1169: rideFile->setDeviceType("Garmin Edge 800"); break;
default: rideFile->setDeviceType(QString("Unknown Garmin Device %1").arg(prod));
}
}
else {
rideFile->setDeviceType(QString("Unknown FIT Device %1:%2").arg(manu).arg(prod));
}
}
void decodeEvent(const FitDefinition &def, int, const std::vector<fit_value_t> values) {
time_t time = 0;
int event = -1;
int event_type = -1;
int i = 0;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++];
if( value == NA_VALUE )
continue;
switch (field.num) {
case 253: time = value + qbase_time.toTime_t(); break;
case 0: event = value; break;
case 1: event_type = value; break;
default: ; // do nothing
}
}
if (event == 0) { // Timer event
switch (event_type) {
case 0: // start
stopped = false;
break;
case 1: // stop
stopped = true;
break;
case 2: // consecutive_depreciated
case 3: // marker
break;
case 4: // stop all
stopped = true;
break;
case 5: // begin_depreciated
case 6: // end_depreciated
case 7: // end_all_depreciated
case 8: // stop_disable
stopped = true;
break;
case 9: // stop_disable_all
stopped = true;
break;
default:
errors << QString("Unknown event type %1").arg(event_type);
}
}
// printf("event type %d\n", event_type);
last_event = event;
last_event_type = event_type;
}
void decodeLap(const FitDefinition &def, int time_offset, const std::vector<fit_value_t> values) {
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
int i = 0;
time_t this_start_time = 0;
++interval;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++];
if( value == NA_VALUE )
continue;
switch (field.num) {
case 253: time = value + qbase_time.toTime_t(); break;
case 2: this_start_time = value + qbase_time.toTime_t(); break;
default: ; // ignore it
}
}
if (this_start_time == 0)
errors << QString("lap %1 has no start time").arg(interval);
else {
rideFile->addInterval(this_start_time - start_time, time - start_time,
QString("%1").arg(interval));
}
}
void decodeRecord(const FitDefinition &def, int time_offset, const std::vector<fit_value_t> values) {
time_t time = 0;
if (time_offset > 0)
time = last_time + time_offset;
double alt = 0, cad = 0, km = 0, grade = 0, hr = 0, lat = 0, lng = 0, badgps = 0;
double resistance = 0, kph = 0, temperature = 0, time_from_course = 0, watts = 0;
fit_value_t lati = NA_VALUE, lngi = NA_VALUE;
int i = 0;
foreach(const FitField &field, def.fields) {
fit_value_t value = values[i++];
if( value == NA_VALUE )
continue;
switch (field.num) {
case 253: time = value + qbase_time.toTime_t();
// Time MUST NOT go backwards
// You canny break the laws of physics, Jim
if (time < last_time) time = last_time;
break;
case 0: lati = value; break;
case 1: lngi = value; break;
case 2: alt = value / 5.0 - 500.0; break;
case 3: hr = value; break;
case 4: cad = value; break;
case 5: km = value / 100000.0; break;
case 6: kph = value * 3.6 / 1000.0; break;
case 7: watts = value; break;
case 8: break; // XXX packed speed/dist
case 9: grade = value / 100.0; break;
case 10: resistance = value; break;
case 11: time_from_course = value / 1000.0; break;
case 12: break; // XXX "cycle_length"
case 13: temperature = value; break;
default: unknown_record_fields.insert(field.num);
}
}
if (time == last_time)
return; // Sketchy, but some FIT files do this.
if (stopped) {
// As it turns out, this happens all the time in some FIT files.
// Since we don't really understand the meaning, don't make noise.
/*
errors << QString("At %1 seconds, time is stopped, but got record "
"anyway. Ignoring it. Last event type was "
"%2 for event %3.").arg(time-start_time).arg(last_event_type).arg(last_event);
return;
*/
}
if (lati != NA_VALUE && lngi != NA_VALUE) {
lat = lati * 180.0 / 0x7fffffff;
lng = lngi * 180.0 / 0x7fffffff;
} else
{
// If lat/lng are missng, set to 0/0 and fill point from last point as 0/0)
lat = 0;
lng = 0;
badgps = 1;
}
if (start_time == 0) {
start_time = time - 1; // XXX: recording interval?
QDateTime t;
t.setTime_t(start_time);
rideFile->setStartTime(t);
}
//printf( "point time=%d lat=%.2lf lon=%.2lf alt=%.1lf hr=%.0lf "
// "cad=%.0lf km=%.1lf kph=%.1lf watts=%.0lf grade=%.1lf "
// "resist=%.1lf off=%.1lf temp=%.1lf\n",
// time, lat, lng, alt, hr,
// cad, km, kph, watts, grade,
// resistance, time_from_course, temperature );
double secs = time - start_time;
double nm = 0;
double headwind = 0.0;
int interval = 0;
if ((last_msg_type == RECORD_TYPE) && (last_time != 0) && (time > last_time + 1)) {
// Evil smart recording. Linearly interpolate missing points.
RideFilePoint *prevPoint = rideFile->dataPoints().back();
int deltaSecs = (int) (secs - prevPoint->secs);
if(deltaSecs != secs - prevPoint->secs)
throw BadDelta(); // no fractional part
// This is only true if the previous record was of type record:
if(deltaSecs != time - last_time)
throw BadDelta();
// If the last lat/lng was missing (0/0) then all points up to lat/lng are marked as 0/0.
if (prevPoint->lat == 0 && prevPoint->lon == 0 ) {
badgps = 1;
}
double deltaCad = cad - prevPoint->cad;
double deltaHr = hr - prevPoint->hr;
double deltaDist = km - prevPoint->km;
if (km < 0.00001) deltaDist = 0.000f; // effectively zero distance
double deltaSpeed = kph - prevPoint->kph;
double deltaTorque = nm - prevPoint->nm;
double deltaPower = watts - prevPoint->watts;
double deltaAlt = alt - prevPoint->alt;
double deltaLon = lng - prevPoint->lon;
double deltaLat = lat - prevPoint->lat;
double deltaHeadwind = headwind - prevPoint->headwind;
for (int i = 1; i < deltaSecs; i++) {
double weight = 1.0 * i / deltaSecs;
rideFile->appendPoint(
prevPoint->secs + (deltaSecs * weight),
prevPoint->cad + (deltaCad * weight),
prevPoint->hr + (deltaHr * weight),
prevPoint->km + (deltaDist * weight),
prevPoint->kph + (deltaSpeed * weight),
prevPoint->nm + (deltaTorque * weight),
prevPoint->watts + (deltaPower * weight),
prevPoint->alt + (deltaAlt * weight),
(badgps == 1) ? 0 : prevPoint->lon + (deltaLon * weight),
(badgps == 1) ? 0 : prevPoint->lat + (deltaLat * weight),
prevPoint->headwind + (deltaHeadwind * weight),
interval);
}
prevPoint = rideFile->dataPoints().back();
}
if (km < 0.00001f) km = last_distance;
rideFile->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, lng, lat, headwind, interval);
last_time = time;
last_distance = km;
}
int read_record(bool &stop, QStringList &errors) {
stop = false;
int count = 0;
int header_byte = read_uint8(&count);
if (!(header_byte & 0x80) && (header_byte & 0x40)) {
// Definition record
int local_msg_type = header_byte & 0xf;
local_msg_types.insert(local_msg_type, FitDefinition());
FitDefinition &def = local_msg_types[local_msg_type];
int reserved = read_uint8(&count); (void) reserved; // unused
def.is_big_endian = read_uint8(&count);
def.global_msg_num = read_uint16(def.is_big_endian, &count);
int num_fields = read_uint8(&count);
//printf("definition: local type=%d global=%d arch=%d fields=%d\n",
// local_msg_type, def.global_msg_num, def.is_big_endian,
// num_fields );
for (int i = 0; i < num_fields; ++i) {
def.fields.push_back(FitField());
FitField &field = def.fields.back();
field.num = read_uint8(&count);
field.size = read_uint8(&count);
int base_type = read_uint8(&count);
field.type = base_type & 0x1f;
//printf(" field %d: %d bytes, num %d, type %d\n",
// i, field.size, field.num, field.type );
}
}
else {
// Data record
int local_msg_type = 0;
int time_offset = 0;
if (header_byte & 0x80) {
// compressed time record
local_msg_type = (header_byte >> 5) & 0x3;
time_offset = header_byte & 0x1f;
}
else {
local_msg_type = header_byte & 0xf;
}
if (!local_msg_types.contains(local_msg_type)) {
errors << QString("local type %1 without previous definition").arg(local_msg_type);
stop = true;
return count;
}
const FitDefinition &def = local_msg_types[local_msg_type];
//printf( "message local=%d global=%d\n", local_msg_type,
// def.global_msg_num );
std::vector<fit_value_t> values;
foreach(const FitField &field, def.fields) {
fit_value_t v;
switch (field.type) {
case 0: v = read_uint8(&count); break;
case 1: v = read_int8(&count); break;
case 2: v = read_uint8(&count); break;
case 3: v = read_int16(def.is_big_endian, &count); break;
case 4: v = read_uint16(def.is_big_endian, &count); break;
case 5: v = read_int32(def.is_big_endian, &count); break;
case 6: v = read_uint32(def.is_big_endian, &count); break;
case 10: v = read_uint8z(&count); break;
case 11: v = read_uint16z(def.is_big_endian, &count); break;
case 12: v = read_uint32z(def.is_big_endian, &count); break;
//XXX: support float, string + byte base types
default:
read_unknown( field.size, &count );
v = NA_VALUE;
unknown_base_type.insert(field.num);
}
values.push_back(v);
//printf( " field: type=%d num=%d value=%lld\n",
// field.type, field.num, v );
}
// Most of the record types in the FIT format aren't actually all
// that useful. FileId, Lap, and Record clearly are. The one
// other one that might be useful is DeviceInfo, but it doesn't
// seem to be filled in properly. Sean's Cinqo, for example,
// shows up as manufacturer #65535, even though it should be #7.
switch (def.global_msg_num) {
case 0: decodeFileId(def, time_offset, values); break;
case 19: decodeLap(def, time_offset, values); break;
case RECORD_TYPE: decodeRecord(def, time_offset, values); break;
case 21: decodeEvent(def, time_offset, values); break;
case 23: /* device info */
case 18: /* session */
case 22: /* undocumented */
case 72: /* undocumented - new for garmin 800*/
case 34: /* activity */
case 49: /* file creator */
case 79: /* unknown */
break;
default:
unknown_global_msg_nums.insert(def.global_msg_num);
}
last_msg_type = def.global_msg_num;
}
return count;
}
RideFile * run() {
rideFile = new RideFile;
rideFile->setDeviceType("Garmin FIT"); // XXX: read from device msg?
rideFile->setRecIntSecs(1.0); // XXX: always?
if (!file.open(QIODevice::ReadOnly)) {
delete rideFile;
return NULL;
}
int header_size = read_uint8();
if (header_size != 12 && header_size != 14) {
errors << QString("bad header size: %1").arg(header_size);
delete rideFile;
return NULL;
}
int protocol_version = read_uint8();
(void) protocol_version;
// if the header size is 14 we have profile minor then profile major
// version. We still don't do anything with this information
int profile_version = read_uint16(false); // always littleEndian
(void) profile_version; // not sure what to do with this
int data_size = read_uint32(false); // always littleEndian
char fit_str[5];
if (file.read(fit_str, 4) != 4) {
errors << "truncated header";
delete rideFile;
return NULL;
}
fit_str[4] = '\0';
if (strcmp(fit_str, ".FIT") != 0) {
errors << QString("bad header, expected \".FIT\" but got \"%1\"").arg(fit_str);
delete rideFile;
return NULL;
}
// read the rest of the header
if (header_size == 14) read_uint16(false);
int bytes_read = 0;
bool stop = false;
try {
while (!stop && (bytes_read < data_size))
bytes_read += read_record(stop, errors);
}
catch (TruncatedRead &e) {
errors << "truncated file body";
delete rideFile;
return NULL;
}
catch (BadDelta &e) {
errors << "Unsupported smart recording interval found";
delete rideFile;
return NULL;
}
if (stop) {
delete rideFile;
return NULL;
}
else {
int crc = read_uint16( false ); // always littleEndian
(void) crc;
foreach(int num, unknown_global_msg_nums)
qDebug() << QString("FitRideFile: unknown global message number %1; ignoring it").arg(num);
foreach(int num, unknown_record_fields)
qDebug() << QString("FitRideFile: unknown record field %1; ignoring it").arg(num);
foreach(int num, unknown_base_type)
qDebug() << QString("FitRideFile: unknown base type %1; skipped").arg(num);
return rideFile;
}
}
};
RideFile *FitFileReader::openRideFile(QFile &file, QStringList &errors) const
{
QSharedPointer<FitFileReaderState> state(new FitFileReaderState(file, errors));
return state->run();
}
// vi:expandtab tabstop=4 shiftwidth=4

29
src/FitRideFile.h Normal file
View File

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

132
src/FixGPS.cpp Normal file
View File

@@ -0,0 +1,132 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "DataProcessor.h"
#include "Settings.h"
#include "Units.h"
#include <algorithm>
#include <QVector>
// Config widget used by the Preferences/Options config panes
class FixGPS;
class FixGPSConfig : public DataProcessorConfig
{
Q_DECLARE_TR_FUNCTIONS(FixGPSConfig)
friend class ::FixGPS;
protected:
public:
// there is no config
FixGPSConfig(QWidget *parent) : DataProcessorConfig(parent) {}
QString explain() {
return(QString(tr("Remove GPS errors and interpolate positional "
"data where the GPS device did not record any data, "
"or the data that was recorded is invalid.")));
}
void readConfig() {}
void saveConfig() {}
};
// RideFile Dataprocessor -- used to handle gaps in recording
// by inserting interpolated/zero samples
// to ensure dataPoints are contiguous in time
//
class FixGPS : public DataProcessor {
Q_DECLARE_TR_FUNCTIONS(FixGPS)
public:
FixGPS() {}
~FixGPS() {}
// the processor
bool postProcess(RideFile *, DataProcessorConfig* config);
// the config widget
DataProcessorConfig* processorConfig(QWidget *parent) {
return new FixGPSConfig(parent);
}
// Localized Name
QString name() {
return (tr("Fix GPS errors"));
}
};
static bool fixGPSAdded = DataProcessorFactory::instance().registerProcessor((QString("Fix GPS errors")), new FixGPS());
bool
FixGPS::postProcess(RideFile *ride, DataProcessorConfig *)
{
// ignore null or files without GPS data
if (!ride || ride->areDataPresent()->lat == false || ride->areDataPresent()->lon == false)
return false;
int errors=0;
ride->command->startLUW("Fix GPS Errors");
int lastgood = -1; // where did we last have decent GPS data?
for (int i=0; i<ride->dataPoints().count(); i++) {
// is this one decent?
if (ride->dataPoints()[i]->lat && ride->dataPoints()[i]->lat >= double(-90) && ride->dataPoints()[i]->lat <= double(90) &&
ride->dataPoints()[i]->lon && ride->dataPoints()[i]->lon >= double(-180) && ride->dataPoints()[i]->lon <= double(180)) {
if (lastgood != -1 && (lastgood+1) != i) {
// interpolate from last good to here
// then set last good to here
double deltaLat = (ride->dataPoints()[i]->lat - ride->dataPoints()[lastgood]->lat) / double(i-lastgood);
double deltaLon = (ride->dataPoints()[i]->lon - ride->dataPoints()[lastgood]->lon) / double(i-lastgood);
for (int j=lastgood+1; j<i; j++) {
ride->command->setPointValue(j, RideFile::lat, ride->dataPoints()[lastgood]->lat + (double(j-lastgood)*deltaLat));
ride->command->setPointValue(j, RideFile::lon, ride->dataPoints()[lastgood]->lon + (double(j-lastgood)*deltaLon));
errors++;
}
} else if (lastgood == -1) {
// fill to front
for (int j=0; j<i; j++) {
ride->command->setPointValue(j, RideFile::lat, ride->dataPoints()[i]->lat);
ride->command->setPointValue(j, RideFile::lon, ride->dataPoints()[i]->lon);
errors++;
}
}
lastgood = i;
}
}
// fill to end...
if (lastgood != -1 && lastgood != (ride->dataPoints().count()-1)) {
// fill from lastgood to end with lastgood
for (int j=lastgood+1; j<ride->dataPoints().count(); j++) {
ride->command->setPointValue(j, RideFile::lat, ride->dataPoints()[lastgood]->lat);
ride->command->setPointValue(j, RideFile::lon, ride->dataPoints()[lastgood]->lon);
errors++;
}
} else {
// they are all bad!!
// XXX do nothing?
}
ride->command->endLUW();
if (errors) {
ride->setTag("GPS errors", QString("%1").arg(errors));
return true;
} else
return false;
}

253
src/FixGaps.cpp Normal file
View File

@@ -0,0 +1,253 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "DataProcessor.h"
#include "Settings.h"
#include "Units.h"
#include <algorithm>
#include <QVector>
// Config widget used by the Preferences/Options config panes
class FixGaps;
class FixGapsConfig : public DataProcessorConfig
{
Q_DECLARE_TR_FUNCTIONS(FixGapsConfig)
friend class ::FixGaps;
protected:
QHBoxLayout *layout;
QLabel *toleranceLabel, *beerandburritoLabel;
QDoubleSpinBox *tolerance,
*beerandburrito;
public:
FixGapsConfig(QWidget *parent) : DataProcessorConfig(parent) {
layout = new QHBoxLayout(this);
layout->setContentsMargins(0,0,0,0);
setContentsMargins(0,0,0,0);
toleranceLabel = new QLabel(tr("Tolerance"));
beerandburritoLabel = new QLabel(tr("Stop"));
tolerance = new QDoubleSpinBox();
tolerance->setMaximum(99.99);
tolerance->setMinimum(0);
tolerance->setSingleStep(0.1);
beerandburrito = new QDoubleSpinBox();
beerandburrito->setMaximum(99999.99);
beerandburrito->setMinimum(0);
beerandburrito->setSingleStep(0.1);
layout->addWidget(toleranceLabel);
layout->addWidget(tolerance);
layout->addWidget(beerandburritoLabel);
layout->addWidget(beerandburrito);
layout->addStretch();
}
//~FixGapsConfig() {} // deliberately not declared since Qt will delete
// the widget and its children when the config pane is deleted
QString explain() {
return(QString(tr("Many devices, especially wireless devices, will "
"drop connections to the bike computer. This leads "
"to lost samples in the resulting data, or so-called "
"drops in recording.\n\n"
"In order to calculate peak powers and averages, it "
"is very helpful to remove these gaps, and either "
"smooth the data where it is missing or just "
"replace with zero value samples\n\n"
"This function performs this task, taking two "
"parameters;\n\n"
"tolerance - this defines the minimum size of a "
"recording gap (in seconds) that will be processed. "
"any gap shorter than this will not be affected.\n\n"
"stop - this defines the maximum size of "
"gap (in seconds) that will have a smoothing algorithm "
"applied. Where a gap is shorter than this value it will "
"be filled with values interpolated from the values "
"recorded before and after the gap. If it is longer "
"than this value, it will be filled with zero values.")));
}
void readConfig() {
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
double tol = settings->value(GC_DPFG_TOLERANCE, "1.0").toDouble();
double stop = settings->value(GC_DPFG_STOP, "1.0").toDouble();
tolerance->setValue(tol);
beerandburrito->setValue(stop);
}
void saveConfig() {
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
settings->setValue(GC_DPFG_TOLERANCE, tolerance->value());
settings->setValue(GC_DPFG_STOP, beerandburrito->value());
}
};
// RideFile Dataprocessor -- used to handle gaps in recording
// by inserting interpolated/zero samples
// to ensure dataPoints are contiguous in time
//
class FixGaps : public DataProcessor {
Q_DECLARE_TR_FUNCTIONS(FixGaps)
public:
FixGaps() {}
~FixGaps() {}
// the processor
bool postProcess(RideFile *, DataProcessorConfig* config);
// the config widget
DataProcessorConfig* processorConfig(QWidget *parent) {
return new FixGapsConfig(parent);
}
// Localized Name
QString name() {
return (tr("Fix Gaps in Recording"));
}
};
static bool fixGapsAdded = DataProcessorFactory::instance().registerProcessor((QString("Fix Gaps in Recording")), new FixGaps());
bool
FixGaps::postProcess(RideFile *ride, DataProcessorConfig *config=0)
{
// get settings
double tolerance, stop;
if (config == NULL) { // being called automatically
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
tolerance = settings->value(GC_DPFG_TOLERANCE, "1.0").toDouble();
stop = settings->value(GC_DPFG_STOP, "1.0").toDouble();
} else { // being called manually
tolerance = ((FixGapsConfig*)(config))->tolerance->value();
stop = ((FixGapsConfig*)(config))->beerandburrito->value();
}
// if the number of duration / number of samples
// equals the recording interval then we don't need
// to post-process for gaps
// XXX commented out since it is not always true and
// is purely to improve performance
//if ((ride->recIntSecs() + ride->dataPoints()[ride->dataPoints().count()-1]->secs -
// ride->dataPoints()[0]->secs) / (double) ride->dataPoints().count() == ride->recIntSecs())
// return false;
// Additionally, If there are less than 2 dataPoints then there
// is no way of post processing anyway (e.g. manual workouts)
if (ride->dataPoints().count() < 2) return false;
// OK, so there are probably some gaps, lets post process them
RideFilePoint *last = NULL;
int dropouts = 0;
double dropouttime = 0.0;
// put it all in a LUW
ride->command->startLUW("Fix Gaps in Recording");
for (int position = 0; position < ride->dataPoints().count(); position++) {
RideFilePoint *point = ride->dataPoints()[position];
if (last == NULL) last = point;
else {
double gap = point->secs - last->secs - ride->recIntSecs();
// if we have gps and we moved, then this isn't a stop
bool stationary = ((last->lat || last->lon) && (point->lat || point->lon)) // gps is present
&& last->lat == point->lat && last->lon == point->lon;
// moved for less than 30 seconds ... interpolate
if (!stationary && gap > tolerance && gap < stop) {
// what's needed?
dropouts++;
dropouttime += gap;
int count = gap/ride->recIntSecs();
double hrdelta = (point->hr - last->hr) / (double) count;
double pwrdelta = (point->watts - last->watts) / (double) count;
double kphdelta = (point->kph - last->kph) / (double) count;
double kmdelta = (point->km - last->km) / (double) count;
double caddelta = (point->cad - last->cad) / (double) count;
double altdelta = (point->alt - last->alt) / (double) count;
double nmdelta = (point->nm - last->nm) / (double) count;
double londelta = (point->lon - last->lon) / (double) count;
double latdelta = (point->lat - last->lat) / (double) count;
double hwdelta = (point->headwind - last->headwind) / (double) count;
// add the points
for(int i=0; i<count; i++) {
RideFilePoint *add = new RideFilePoint(last->secs+((i+1)*ride->recIntSecs()),
last->cad+((i+1)*caddelta),
last->hr + ((i+1)*hrdelta),
last->km + ((i+1)*kmdelta),
last->kph + ((i+1)*kphdelta),
last->nm + ((i+1)*nmdelta),
last->watts + ((i+1)*pwrdelta),
last->alt + ((i+1)*altdelta),
last->lon + ((i+1)*londelta),
last->lat + ((i+1)*latdelta),
last->headwind + ((i+1)*hwdelta),
last->interval);
ride->command->insertPoint(position++, add);
}
// stationary or greater than 30 seconds... fill with zeroes
} else if (gap > stop) {
dropouts++;
dropouttime += gap;
int count = gap/ride->recIntSecs();
double kmdelta = (point->km - last->km) / (double) count;
// add zero value points
for(int i=0; i<count; i++) {
RideFilePoint *add = new RideFilePoint(last->secs+((i+1)*ride->recIntSecs()),
0,
0,
last->km + ((i+1)*kmdelta),
0,
0,
0,
last->alt,
0,
0,
0,
last->interval);
ride->command->insertPoint(position++, add);
}
}
}
last = point;
}
// end the Logical unit of work here
ride->command->endLUW();
ride->setTag("Dropouts", QString("%1").arg(dropouts));
ride->setTag("Dropout Time", QString("%1").arg(dropouttime));
if (dropouts) return true;
else return false;
}

203
src/FixSpikes.cpp Normal file
View File

@@ -0,0 +1,203 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "DataProcessor.h"
#include "LTMOutliers.h"
#include "Settings.h"
#include "Units.h"
#include <algorithm>
#include <QVector>
// Config widget used by the Preferences/Options config panes
class FixSpikes;
class FixSpikesConfig : public DataProcessorConfig
{
Q_DECLARE_TR_FUNCTIONS(FixSpikesConfig)
friend class ::FixSpikes;
protected:
QHBoxLayout *layout;
QLabel *maxLabel, *varianceLabel;
QDoubleSpinBox *max,
*variance;
public:
FixSpikesConfig(QWidget *parent) : DataProcessorConfig(parent) {
layout = new QHBoxLayout(this);
layout->setContentsMargins(0,0,0,0);
setContentsMargins(0,0,0,0);
maxLabel = new QLabel(tr("Max"));
varianceLabel = new QLabel(tr("Variance"));
max = new QDoubleSpinBox();
max->setMaximum(9999.99);
max->setMinimum(0);
max->setSingleStep(1);
variance = new QDoubleSpinBox();
variance->setMaximum(9999);
variance->setMinimum(0);
variance->setSingleStep(50);
layout->addWidget(maxLabel);
layout->addWidget(max);
layout->addWidget(varianceLabel);
layout->addWidget(variance);
layout->addStretch();
}
//~FixSpikesConfig() {} // deliberately not declared since Qt will delete
// the widget and its children when the config pane is deleted
QString explain() {
return(QString(tr("Occasionally power meters will erroneously "
"report high values for power. For crank based "
"power meters such as SRM and Quarq this is "
"caused by an erroneous cadence reading "
"as a result of triggering a reed switch "
"whilst pushing off\n\n"
"This function will look for spikes/anomalies "
"in power data and replace the erroneous data "
"by smoothing/interpolating the data from either "
"side of the point in question\n\n"
"It takes the following parameters:\n\n"
"Absolute Max - this defines an absolute value "
"for watts, and will smooth any values above this "
"absolute value that have been identified as being "
"anomalies (i.e. at odds with the data surrounding it)\n\n"
"Variance (%) - this will smooth any values which "
"are higher than this percentage of the rolling "
"average wattage for the 30 seconds leading up "
"to the spike.")));
}
void readConfig() {
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
double tol = settings->value(GC_DPFS_MAX, "1500").toDouble();
double stop = settings->value(GC_DPFS_VARIANCE, "1000").toDouble();
max->setValue(tol);
variance->setValue(stop);
}
void saveConfig() {
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
settings->setValue(GC_DPFS_MAX, max->value());
settings->setValue(GC_DPFS_VARIANCE, variance->value());
}
};
// RideFile Dataprocessor -- used to handle gaps in recording
// by inserting interpolated/zero samples
// to ensure dataPoints are contiguous in time
//
class FixSpikes : public DataProcessor {
Q_DECLARE_TR_FUNCTIONS(FixSpikes)
public:
FixSpikes() {}
~FixSpikes() {}
// the processor
bool postProcess(RideFile *, DataProcessorConfig* config);
// the config widget
DataProcessorConfig* processorConfig(QWidget *parent) {
return new FixSpikesConfig(parent);
}
// Localized Name
QString name() {
return (tr("Fix Power Spikes"));
}
};
static bool fixSpikesAdded = DataProcessorFactory::instance().registerProcessor((QString("Fix Power Spikes")), new FixSpikes());
bool
FixSpikes::postProcess(RideFile *ride, DataProcessorConfig *config=0)
{
// does this ride have power?
if (ride->areDataPresent()->watts == false) return false;
// get settings
double variance, max;
if (config == NULL) { // being called automatically
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
max = settings->value(GC_DPFS_MAX, "1500").toDouble();
variance = settings->value(GC_DPFS_VARIANCE, "1000").toDouble();
} else { // being called manually
max = ((FixSpikesConfig*)(config))->max->value();
variance = ((FixSpikesConfig*)(config))->variance->value();
}
int windowsize = 30 / ride->recIntSecs();
// We use a window size of 30s to find spikes
// if the ride is shorter, don't bother
// is no way of post processing anyway (e.g. manual workouts)
if (windowsize > ride->dataPoints().count()) return false;
// Find the power outliers
int spikes = 0;
double spiketime = 0.0;
// create a data array for the outlier algorithm
QVector<double> power;
QVector<double> secs;
foreach (RideFilePoint *point, ride->dataPoints()) {
power.append(point->watts);
secs.append(point->secs);
}
LTMOutliers *outliers = new LTMOutliers(secs.data(), power.data(), power.count(), windowsize, false);
ride->command->startLUW("Fix Spikes in Recording");
for (int i=0; i<secs.count(); i++) {
// is this over variance threshold?
if (outliers->getDeviationForRank(i) < variance) break;
// ok, so its highly variant but is it over
// the max value we are willing to accept?
if (outliers->getYForRank(i) < max) continue;
// Houston, we have a spike
spikes++;
spiketime += ride->recIntSecs();
// which one is it
int pos = outliers->getIndexForRank(i);
double left=0.0, right=0.0;
if (pos > 0) left = ride->dataPoints()[pos-1]->watts;
if (pos < (ride->dataPoints().count()-1)) right = ride->dataPoints()[pos+1]->watts;
ride->command->setPointValue(pos, RideFile::watts, (left+right)/2.0);
}
ride->command->endLUW();
ride->setTag("Spikes", QString("%1").arg(spikes));
ride->setTag("Spike Time", QString("%1").arg(spiketime));
if (spikes) return true;
else return false;
}

155
src/FixTorque.cpp Normal file
View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "DataProcessor.h"
#include "LTMOutliers.h"
#include "Settings.h"
#include "Units.h"
#include <algorithm>
#include <QVector>
// Config widget used by the Preferences/Options config panes
class FixTorque;
class FixTorqueConfig : public DataProcessorConfig
{
Q_DECLARE_TR_FUNCTIONS(FixTorqueConfig)
friend class ::FixTorque;
protected:
QHBoxLayout *layout;
QLabel *taLabel;
QLineEdit *ta;
public:
FixTorqueConfig(QWidget *parent) : DataProcessorConfig(parent) {
layout = new QHBoxLayout(this);
layout->setContentsMargins(0,0,0,0);
setContentsMargins(0,0,0,0);
taLabel = new QLabel(tr("Torque Adjust"));
ta = new QLineEdit();
layout->addWidget(taLabel);
layout->addWidget(ta);
layout->addStretch();
}
//~FixTorqueConfig() {} // deliberately not declared since Qt will delete
// the widget and its children when the config pane is deleted
QString explain() {
return(QString(tr("Adjusting torque values allows you to "
"uplift or degrade the torque values when the calibration "
"of your power meter was incorrect. It "
"takes a single parameter:\n\n"
"Torque Adjust - this defines an absolute value "
"in poinds per square inch or newton meters to "
"modify values by. Negative values are supported. (e.g. enter \"1.2 nm\" or "
"\"-0.5 pi\").")));
}
void readConfig() {
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
ta->setText(settings->value(GC_DPTA, "0 nm").toString());
}
void saveConfig() {
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
settings->setValue(GC_DPTA, ta->text());
}
};
// RideFile Dataprocessor -- used to handle gaps in recording
// by inserting interpolated/zero samples
// to ensure dataPoints are contiguous in time
//
class FixTorque : public DataProcessor {
Q_DECLARE_TR_FUNCTIONS(FixTorque)
public:
FixTorque() {}
~FixTorque() {}
// the processor
bool postProcess(RideFile *, DataProcessorConfig* config);
// the config widget
DataProcessorConfig* processorConfig(QWidget *parent) {
return new FixTorqueConfig(parent);
}
// Localized Name
QString name() {
return (tr("Adjust Torque Values"));
}
};
static bool fixTorqueAdded = DataProcessorFactory::instance().registerProcessor((QString("Adjust Torque Values")), new FixTorque());
bool
FixTorque::postProcess(RideFile *ride, DataProcessorConfig *config=0)
{
// does this ride have torque?
if (ride->areDataPresent()->nm == false) return false;
// Lets do it then!
QString ta;
double nmAdjust;
if (config == NULL) { // being called automatically
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
ta = settings->value(GC_DPTA, "0 nm").toString();
} else { // being called manually
ta = ((FixTorqueConfig*)(config))->ta->text();
}
// patrick's torque adjustment code
bool pi = ta.endsWith("pi", Qt::CaseInsensitive);
if (pi || ta.endsWith("nm", Qt::CaseInsensitive)) {
nmAdjust = ta.left(ta.length() - 2).toDouble();
if (pi) {
nmAdjust *= 0.11298482933;
}
} else {
nmAdjust = ta.toDouble();
}
// no adjustment required
if (nmAdjust == 0) return false;
// apply the change
ride->command->startLUW("Adjust Torque");
for (int i=0; i<ride->dataPoints().count(); i++) {
RideFilePoint *point = ride->dataPoints()[i];
if (point->nm != 0) {
double newnm = point->nm + nmAdjust;
ride->command->setPointValue(i, RideFile::watts, point->watts * (newnm / point->nm));
ride->command->setPointValue(i, RideFile::nm, newnm);
}
}
ride->command->endLUW();
double currentta = ride->getTag("Torque Adjust", "0.0").toDouble();
ride->setTag("Torque Adjust", QString("%1 nm").arg(currentta + nmAdjust));
return true;
}

View File

@@ -22,6 +22,8 @@
#include <QVector>
#include <assert.h>
#include <QDebug>
#define DATETIME_FORMAT "yyyy/MM/dd hh:mm:ss' UTC'"
static int gcFileReaderRegistered =
@@ -54,8 +56,50 @@ GcFileReader::openRideFile(QFile &file, QStringList &errors) const
QString value = attr.attribute("value");
if (key == "Device type")
rideFile->setDeviceType(value);
if (key == "Start time")
rideFile->setStartTime(QDateTime::fromString(value, DATETIME_FORMAT).toLocalTime());
if (key == "Start time") {
// by default QDateTime is localtime - the source however is UTC
QDateTime aslocal = QDateTime::fromString(value, DATETIME_FORMAT);
// construct in UTC so we can honour the conversion to localtime
QDateTime asUTC = QDateTime(aslocal.date(), aslocal.time(), Qt::UTC);
// now set in localtime
rideFile->setStartTime(asUTC.toLocalTime());
}
}
// read in metric overrides:
// <override>
// <metric name="skiba_bike_score" value="100"/>
// <metric name="average_speed" secs="3600" km="30"/>
// </override>
QDomNode overrides = root.firstChildElement("override");
if (!overrides.isNull()) {
for (QDomElement override = overrides.firstChildElement("metric");
!override.isNull();
override = override.nextSiblingElement("metric")) {
// setup the metric overrides QMap
QMap<QString, QString> bsm;
// for now only value is known to be maintained
bsm.insert("value", override.attribute("value"));
// insert into the rideFile overrides
rideFile->metricOverrides.insert(override.attribute("name"), bsm);
}
}
// read in the name/value metadata pairs
QDomNode tags = root.firstChildElement("tags");
if (!tags.isNull()) {
for (QDomElement tag = tags.firstChildElement("tag");
!tag.isNull();
tag = tag.nextSiblingElement("tag")) {
rideFile->setTag(tag.attribute("name"), tag.attribute("value"));
}
}
QVector<double> intervalStops; // used to set the interval number for each point
@@ -80,15 +124,13 @@ GcFileReader::openRideFile(QFile &file, QStringList &errors) const
int interval = 0;
QDomElement samples = root.firstChildElement("samples");
if (samples.isNull()) {
errors << "no sample section in ride file";
return NULL;
}
if (samples.isNull()) return rideFile; // manual file will have no samples
bool recIntSet = false;
for (QDomElement sample = samples.firstChildElement("sample");
!sample.isNull(); sample = sample.nextSiblingElement("sample")) {
double secs, cad, hr, km, kph, nm, watts, alt, lon, lat;
double headwind = 0.0;
secs = sample.attribute("secs", "0.0").toDouble();
cad = sample.attribute("cad", "0.0").toDouble();
hr = sample.attribute("hr", "0.0").toDouble();
@@ -101,7 +143,7 @@ GcFileReader::openRideFile(QFile &file, QStringList &errors) const
lat = sample.attribute("lat", "0.0").toDouble();
while ((interval < intervalStops.size()) && (secs >= intervalStops[interval]))
++interval;
rideFile->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, lon, lat, interval);
rideFile->appendPoint(secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, interval);
if (!recIntSet) {
rideFile->setRecIntSecs(sample.attribute("len").toDouble());
recIntSet = true;
@@ -116,10 +158,15 @@ GcFileReader::openRideFile(QFile &file, QStringList &errors) const
return rideFile;
}
// normal precision (Qt defaults)
#define add_sample(name) \
if (present->name) \
sample.setAttribute(#name, QString("%1").arg(point->name));
// high precision (6 decimals)
#define add_sample_hp(name) \
if (present->name) \
sample.setAttribute(#name, QString("%1").arg(point->name, 0, 'g', 11));
void
GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const
{
@@ -140,6 +187,45 @@ GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const
attribute.setAttribute("key", "Device type");
attribute.setAttribute("value", ride->deviceType());
// write out in metric overrides:
// <override>
// <metric name="skiba_bike_score" value="100"/>
// <metric name="average_speed" secs="3600" km="30"/>
// </override>
// write out the QMap tag/value pairs
QDomElement overrides = doc.createElement("override");
root.appendChild(overrides);
QMap<QString,QMap<QString, QString> >::const_iterator k;
for (k=ride->metricOverrides.constBegin(); k != ride->metricOverrides.constEnd(); k++) {
// may not contain anything
if (k.value().isEmpty()) continue;
QDomElement override = doc.createElement("metric");
overrides.appendChild(override);
// metric name
override.setAttribute("name", k.key());
// key/value pairs
QMap<QString, QString>::const_iterator j;
for (j=k.value().constBegin(); j != k.value().constEnd(); j++) {
override.setAttribute(j.key(), j.value());
}
}
// write out the QMap tag/value pairs
QDomElement tags = doc.createElement("tags");
root.appendChild(tags);
QMap<QString,QString>::const_iterator i;
for (i=ride->tags().constBegin(); i != ride->tags().constEnd(); i++) {
QDomElement tag = doc.createElement("tag");
tags.appendChild(tag);
tag.setAttribute("name", i.key());
tag.setAttribute("value", i.value());
}
if (!ride->intervals().empty()) {
QDomElement intervals = doc.createElement("intervals");
root.appendChild(intervals);
@@ -161,7 +247,7 @@ GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const
QDomElement sample = doc.createElement("sample");
samples.appendChild(sample);
assert(present->secs);
add_sample(secs);
add_sample_hp(secs);
add_sample(cad);
add_sample(hr);
add_sample(km);
@@ -169,8 +255,8 @@ GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const
add_sample(nm);
add_sample(watts);
add_sample(alt);
add_sample(lon);
add_sample(lat);
add_sample_hp(lon);
add_sample_hp(lat);
sample.setAttribute("len", QString("%1").arg(ride->recIntSecs()));
}
}

627
src/GoogleMapControl.cpp Normal file
View File

@@ -0,0 +1,627 @@
/*
* Copyright (c) 2009 Greg Lonnon (greg.lonnon@gmail.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 "GoogleMapControl.h"
#include "RideItem.h"
#include "RideFile.h"
#include "MainWindow.h"
#include "Zones.h"
#include "Settings.h"
#include "Units.h"
#include "TimeUtils.h"
#include <QDebug>
#include <string>
#include <vector>
#include <algorithm>
#include <boost/foreach.hpp>
#include <boost/circular_buffer.hpp>
using namespace std;
namespace gm
{
// quick ideas on a math pipeline, kindof like this...
// but more of a pipeline...
// it makes the math somewhat easier to do and understand...
class RideFilePointAlgorithm
{
protected:
RideFilePoint prevRfp;
bool first;
RideFilePointAlgorithm() { first = false; }
};
// Algorithm to find the meidan of a set of data
template<typename T> class Median
{
boost::circular_buffer<T> buffer;
public:
Median(int size)
{
buffer.set_capacity(size);
}
// add the new data
void add(T a) { buffer.push_back(a); }
operator T()
{
if(buffer.size() == 0)
{
return 0;
}
T total = 0;
BOOST_FOREACH(T point, buffer)
{
total += point;
}
return total / buffer.size();
}
};
class AltGained : private RideFilePointAlgorithm
{
protected:
double gained;
double curAlt, prevAlt;
Median<double> median;
public:
AltGained(): gained(0), curAlt(0), prevAlt(0), median(20) {}
void operator()(RideFilePoint rfp)
{
median.add(rfp.alt);
curAlt = median;
if(prevAlt == 0)
{
prevAlt = median;
}
if(curAlt> prevAlt)
{
gained += curAlt - prevAlt;
}
prevAlt = curAlt;
}
int TotalGained() { return gained; }
operator int() { return TotalGained(); }
};
class AvgHR
{
int samples;
int totalHR;
public:
AvgHR() : samples(0),totalHR(0.0) {}
void operator()(RideFilePoint rfp)
{
totalHR += rfp.hr;
samples++;
}
int HR() {
if(samples == 0) return 0;
return totalHR / samples;
}
operator int() { return HR(); }
};
class AvgPower
{
int samples;
double totalPower;
public:
AvgPower() : samples(0), totalPower(0.0) { }
void operator()(RideFilePoint rfp)
{
totalPower += rfp.watts;
samples++;
}
int Power() { return (int) (totalPower / samples); }
operator int() { return Power(); }
};
}
using namespace gm;
#define GOOGLE_KEY "ABQIAAAAS9Z2oFR8vUfLGYSzz40VwRQ69UCJw2HkJgivzGoninIyL8-QPBTtnR-6pM84ljHLEk3PDql0e2nJmg"
GoogleMapControl::GoogleMapControl(MainWindow *mw) : main(mw), range(-1), current(NULL)
{
parent = mw;
view = new QWebView();
layout = new QVBoxLayout();
layout->addWidget(view);
setLayout(layout);
connect(parent, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
connect(view, SIGNAL(loadStarted()), this, SLOT(loadStarted()));
connect(view, SIGNAL(loadFinished(bool)), this, SLOT(loadFinished(bool)));
loadingPage = false;
newRideToLoad = false;
}
void
GoogleMapControl::rideSelected()
{
if (parent->activeTab() != this) return;
RideItem * ride = parent->rideItem();
if (ride == current || !ride || !ride->ride())
return;
else
current = ride;
range =ride->zoneRange();
if(range < 0)
{
rideCP = 300; // default cp to 300 watts
}
else
{
rideCP = ride->zones->getCP(range);
}
rideData.clear();
double prevLon = 1000;
double prevLat = 1000;
foreach(RideFilePoint *rfp,ride->ride()->dataPoints())
{
RideFilePoint curRfp = *rfp;
// wko imports can have -320 type of values for roughly 40 degrees
// This if range is a guess that we only have these -ve type numbers for actual 0 to 90 deg of Latitude
if(curRfp.lat <= -270 && curRfp.lat >= -360)
{
curRfp.lat = 360 + curRfp.lat;
}
// Longitude = -180 to 180 degrees, Latitude = -90 to 90 degrees - anything else is invalid.
if((curRfp.lon < -180 || curRfp.lon > 180 || curRfp.lat < -90 || curRfp.lat > 90)
// Garmin FIT records a missed GPS signal as 0/0.
|| ((curRfp.lon == 0) && (curRfp.lat == 0)))
{
curRfp.lon = 999;
curRfp.lat = 999;
}
prevLon = curRfp.lon;
prevLat = curRfp.lat;
rideData.push_back(curRfp);
}
newRideToLoad = true;
loadRide();
}
void GoogleMapControl::resizeEvent(QResizeEvent * )
{
static bool first = true;
if (parent->activeTab() != this) return;
if (first == true) {
first = false;
} else {
newRideToLoad = true;
loadRide();
}
}
void GoogleMapControl::loadStarted()
{
loadingPage = true;
}
/// called after the load is finished
void GoogleMapControl::loadFinished(bool)
{
loadingPage = false;
loadRide();
}
void GoogleMapControl::loadRide()
{
if(loadingPage == true)
{
return;
}
if(newRideToLoad == true)
{
createHtml(current);
newRideToLoad = false;
loadingPage = true;
QString htmlFile(QDir::tempPath());
htmlFile.append("/maps.html");
QFile file(htmlFile);
file.remove();
file.open(QIODevice::ReadWrite);
file.write(currentPage.str().c_str(),currentPage.str().length());
file.flush();
file.close();
QString filename("file:///");
filename.append(htmlFile);
QUrl url(filename);
view->load(url);
}
}
void GoogleMapControl::createHtml(RideItem *ride)
{
currentPage.str("");
double minLat, minLon, maxLat, maxLon;
minLat = minLon = 1000;
maxLat = maxLon = -1000; // larger than 360
BOOST_FOREACH(RideFilePoint rfp, rideData)
{
if (rfp.lat != 999 && rfp.lon != 999)
{
minLat = std::min(minLat,rfp.lat);
maxLat = std::max(maxLat,rfp.lat);
minLon = std::min(minLon,rfp.lon);
maxLon = std::max(maxLon,rfp.lon);
}
}
if(minLon == 1000)
{
currentPage << tr("No GPS Data Present").toStdString();
return;
}
/// seems to be the magic number... to stop the scrollbars
int width = view->width() -16;
int height = view->height() -16;
std::ostringstream oss;
oss.precision(6);
oss.setf(ios::fixed,ios::floatfield);
oss << "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"" << endl
<< "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" << endl
<< "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\">" << endl
<< "<head>" << endl
<< "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>" << endl
<< "<title>GoldenCheetah</title>" << endl
<< "<script src=\"http://maps.google.com/maps?file=api&amp;v=2&amp;key=" << GOOGLE_KEY <<"\"" << endl
<< "type=\"text/javascript\"></script>" << endl
<< CreateMapToolTipJavaScript() << endl
<< "<script type=\"text/javascript\">"<< endl
<< "var map;" << endl
<< "function initialize() {" << endl
<< "if (GBrowserIsCompatible()) {" << endl
<< "map = new GMap2(document.getElementById(\"map_canvas\")," << endl
<< " {size: new GSize(" << width << "," << height << ") } );" << endl
<< "map.setUIToDefault()" << endl
<< CreatePolyLine() << endl
<< CreateMarkers(ride) << endl
<< "var sw = new GLatLng(" << minLat << "," << minLon << ");" << endl
<< "var ne = new GLatLng(" << maxLat << "," << maxLon << ");" << endl
<< "var bounds = new GLatLngBounds(sw,ne);" << endl
<< "var zoomLevel = map.getBoundsZoomLevel(bounds);" << endl
<< "var center = bounds.getCenter(); " << endl
<< "map.setCenter(bounds.getCenter(),map.getBoundsZoomLevel(bounds),G_PHYSICAL_MAP);" << endl
<< "map.enableScrollWheelZoom();" << endl
<< "}" << endl
<< "}" << endl
<< "function animate() {" << endl
<< "map.panTo(new GLatLng(" << maxLat << "," << minLon << "));" << endl
<< "}" << endl
<< "</script>" << endl
<< "</head>" << endl
<< "<body onload=\"initialize()\" onunload=\"GUnload()\">" << endl
<< "<div id=\"map_canvas\" style=\"width: " << width <<"px; height: "
<< height <<"px\"></div>" << endl
<< "<form action=\"#\" onsubmit=\"animate(); return false\">" << endl
<< "</form>" << endl
<< "</body>" << endl
<< "</html>" << endl;
currentPage << oss.str();
}
QColor GoogleMapControl::GetColor(int watts)
{
if (range < 0) return Qt::red;
else return zoneColor(main->zones()->whichZone(range, watts), 7);
}
/// create the ride line
string GoogleMapControl::CreatePolyLine()
{
std::vector<RideFilePoint> intervalPoints;
ostringstream oss;
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant intervalTime = settings->value(GC_MAP_INTERVAL).toInt();
if (intervalTime.isNull() || intervalTime.toInt() == 0)
intervalTime.setValue(30);
BOOST_FOREACH(RideFilePoint rfp, rideData)
{
intervalPoints.push_back(rfp);
if((intervalPoints.back().secs - intervalPoints.front().secs) >
intervalTime.toInt())
{
// find the avg power and color code it and create a polyline...
AvgPower avgPower = for_each(intervalPoints.begin(),
intervalPoints.end(),
AvgPower());
// find the color
// create the polyline
CreateSubPolyLine(intervalPoints,oss,avgPower);
intervalPoints.clear();
intervalPoints.push_back(rfp);
}
}
return oss.str();
}
void GoogleMapControl::CreateSubPolyLine(const std::vector<RideFilePoint> &points,
std::ostringstream &oss,
int avgPower)
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant intervalTime = settings->value(GC_MAP_INTERVAL);
if (intervalTime.isNull() || intervalTime.toInt() == 0)
intervalTime.setValue(30);
oss.precision(6);
QColor color = GetColor(avgPower);
QString colorstr = color.name();
oss.setf(ios::fixed,ios::floatfield);
oss << "var polyline = new GPolyline([";
BOOST_FOREACH(RideFilePoint rfp, points)
{
if (!(rfp.lat == 999 && rfp.lon == 999))
{
oss << "new GLatLng(" << rfp.lat << "," << rfp.lon << ")," << endl;
}
}
oss << "],\"" << colorstr.toStdString() << "\",4);" << endl;
oss << "GEvent.addListener(polyline, 'mouseover', function() {" << endl
<< "var tooltip_text = '" << intervalTime.toInt() << "s Power: " << avgPower << "';" << endl
<< "var ss={'weight':8};" << endl
<< "this.setStrokeStyle(ss);" << endl
<< "this.overlay = new MapTooltip(this,tooltip_text);" << endl
<< "map.addOverlay(this.overlay);" << endl
<< "});" << endl
<< "GEvent.addListener(polyline, 'mouseout', function() {" << endl
<< "map.removeOverlay(this.overlay);" << endl
<< "var ss={'weight':5};" << endl
<< "this.setStrokeStyle(ss);" << endl
<< "});" << endl;
oss << "map.addOverlay (polyline);" << endl;
}
string GoogleMapControl::CreateIntervalMarkers(RideItem *rideItem)
{
ostringstream oss;
RideFile *ride = rideItem->ride();
if(ride->intervals().size() == 0)
return "";
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant unit = settings->value(GC_UNIT);
bool metricUnits = (unit.toString() == "Metric");
QString s;
if (settings->contains(GC_SETTINGS_INTERVAL_METRICS))
s = settings->value(GC_SETTINGS_INTERVAL_METRICS).toString();
else
s = GC_SETTINGS_INTERVAL_METRICS_DEFAULT;
QStringList intervalMetrics = s.split(",");
int row = 0;
foreach (RideFileInterval interval, ride->intervals()) {
// create a temp RideFile to be used to figure out the metrics for the interval
RideFile f(ride->startTime(), ride->recIntSecs());
for (int i = ride->intervalBegin(interval); i < ride->dataPoints().size(); ++i) {
const RideFilePoint *p = ride->dataPoints()[i];
if (p->secs >= interval.stop)
break;
f.appendPoint(p->secs, p->cad, p->hr, p->km, p->kph, p->nm,
p->watts, p->alt, p->lon, p->lat, p->headwind, 0);
}
if (f.dataPoints().size() == 0) {
// Interval empty, do not compute any metrics
continue;
}
QHash<QString,RideMetricPtr> metrics =
RideMetric::computeMetrics(&f, parent->zones(), parent->hrZones(), intervalMetrics);
string html = CreateIntervalHtml(metrics,intervalMetrics,interval.name,metricUnits);
row++;
oss << CreateMarker(row,f.dataPoints().front()->lat,f.dataPoints().front()->lon,html);
}
return oss.str();
}
string GoogleMapControl::CreateIntervalHtml(QHash<QString,RideMetricPtr> &metrics, QStringList &intervalMetrics,
QString &intervalName, bool metricUnits)
{
ostringstream oss;
oss << "<p><h2>" << intervalName.toStdString() << "</h2>";
oss << "<table align=\\\"center\\\" cellspacing=0 border=0>";
int row = 0;
foreach (QString symbol, intervalMetrics) {
RideMetricPtr m = metrics.value(symbol);
if(m == NULL)
continue;
if (row % 2)
oss << "<tr>";
else {
QColor color = QApplication::palette().alternateBase().color();
color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value());
oss << "<tr bgcolor='" + color.name().toStdString() << "'>";
}
oss.setf(ios::fixed);
oss.precision(m->precision());
oss << "<td align=\\\"left\\\">" + m->name().toStdString() << "</td>";
oss << "<td align=\\\"left\\\">";
if (m->units(metricUnits) == "seconds" || m->units(metricUnits) == tr("seconds"))
oss << time_to_string(m->value(metricUnits)).toStdString();
else
{
oss << m->value(metricUnits);
oss << " " << m->units(metricUnits).toStdString();
}
oss <<"</td>";
oss << "</tr>";
row++;
}
oss << "</table>";
return oss.str();
}
string GoogleMapControl::CreateMarkers(RideItem *ride)
{
ostringstream oss;
oss.precision(6);
oss.setf(ios::fixed,ios::floatfield);
// start marker
/*
oss << "var marker;" << endl;
oss << "var greenIcon = new GIcon(G_DEFAULT_ICON);" << endl;
oss << "greenIcon.image = \"http://gmaps-samples.googlecode.com/svn/trunk/markers/green/blank.png\"" << endl;
oss << "markerOptions = { icon:greenIcon };" << endl;
oss << "marker = new GMarker(new GLatLng(";
oss << rideData.front().lat << "," << rideData.front().lon << "),greenIcon);" << endl;
oss << "marker.bindInfoWindowHtml(\"<h3>Start</h3>\");" << endl;
oss << "map.addOverlay(marker);" << endl;
*/
oss << CreateIntervalMarkers(ride);
// end marker
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
QVariant unit = settings->value(GC_UNIT);
bool metricUnits = (unit.toString() == "Metric");
QString s;
if (settings->contains(GC_SETTINGS_INTERVAL_METRICS))
s = settings->value(GC_SETTINGS_INTERVAL_METRICS).toString();
else
s = GC_SETTINGS_INTERVAL_METRICS_DEFAULT;
QStringList intervalMetrics = s.split(",");
QHash<QString,RideMetricPtr> metrics =
RideMetric::computeMetrics(ride->ride(), parent->zones(), parent->hrZones(), intervalMetrics);
QString name = "Ride Summary";
string html = CreateIntervalHtml(metrics,intervalMetrics,name,metricUnits);
oss << "var redIcon = new GIcon(G_DEFAULT_ICON);" << endl;
oss << "redIcon.image = \"http://gmaps-samples.googlecode.com/svn/trunk/markers/red/blank.png\"" << endl;
oss << "markerOptions = { icon:redIcon };" << endl;
oss << "marker = new GMarker(new GLatLng(";
oss << rideData.back().lat << "," << rideData.back().lon << "),redIcon);" << endl;
oss << "marker.bindInfoWindowHtml(\""<< html << "\");" << endl;
oss << "map.addOverlay(marker);" << endl;
return oss.str();
}
std::string GoogleMapControl::CreateMarker(int number, double lat, double lon, string &html)
{
ostringstream oss;
oss.precision(6);
oss << "intervalIcon = new GIcon(G_DEFAULT_ICON);" << endl;
if ( number == 1 )
oss << "intervalIcon.image = \"http://gmaps-samples.googlecode.com/svn/trunk/markers/green/marker" << number << ".png\"" << endl;
else
oss << "intervalIcon.image = \"http://gmaps-samples.googlecode.com/svn/trunk/markers/blue/marker" << number << ".png\"" << endl;
oss << "markerOptions = { icon:intervalIcon };" << endl;
oss << "marker = new GMarker(new GLatLng( ";
oss<< lat << "," << lon << "),intervalIcon);" << endl;
oss << "marker.bindInfoWindowHtml(\"" << html << "\");";
oss.precision(1);
oss << "map.addOverlay(marker);" << endl;
return oss.str();
}
string GoogleMapControl::CreateMapToolTipJavaScript()
{
ostringstream oss;
oss << "<script type=\"text/javascript\">" << endl
<< "var MapTooltip = function(obj, html, options) {"<< endl
<< "this.obj = obj;"<< endl
<< "this.html = html;"<< endl
<< "this.options = options || {};"<< endl
<< "}"<< endl
<< "MapTooltip.prototype = new GOverlay();"<< endl
<< "MapTooltip.prototype.initialize = function(map) {"<< endl
<< "var div = document.getElementById('MapTooltipContainer');"<< endl
<< "var that = this;"<< endl
<< "if (!div) {"<< endl
<< "var div = document.createElement('div');"<< endl
<< "div.setAttribute('id', 'MapTooltipContainer');"<< endl
<< "}"<< endl
<< "if (this.options.maxWidth || this.options.minWidth) {"<< endl
<< "div.style.maxWidth = this.options.maxWidth || '150px';"<< endl
<< "div.style.minWidth = this.options.minWidth || '150px';"<< endl
<< "} else {"<< endl
<< "div.style.width = this.options.width || '150px';"<< endl
<< "}"<< endl
<< "div.style.padding = this.options.padding || '5px';"<< endl
<< "div.style.backgroundColor = this.options.backgroundColor || '#ffffff';"<< endl
<< "div.style.border = this.options.border || '1px solid #000000';"<< endl
<< "div.style.fontSize = this.options.fontSize || '80%';"<< endl
<< "div.style.color = this.options.color || '#000';"<< endl
<< "div.innerHTML = this.html;"<< endl
<< "div.style.position = 'absolute';"<< endl
<< "div.style.zIndex = '1000';"<< endl
<< "var offsetX = this.options.offsetX || 10;"<< endl
<< "var offsetY = this.options.offsetY || 0;"<< endl
<< "var bounds = map.getBounds();"<< endl
<< "rightEdge = map.fromLatLngToDivPixel(bounds.getNorthEast()).x;"<< endl
<< "bottomEdge = map.fromLatLngToDivPixel(bounds.getSouthWest()).y;"<< endl
<< "var mapev = GEvent.addListener(map, 'mousemove', function(latlng) {"<< endl
<< "GEvent.removeListener(mapev);"<< endl
<< "var pixelPosX = (map.fromLatLngToDivPixel(latlng)).x + offsetX;"<< endl
<< "var pixelPosY = (map.fromLatLngToDivPixel(latlng)).y - offsetY;"<< endl
<< "div.style.left = pixelPosX + 'px';"<< endl
<< "div.style.top = pixelPosY + 'px';"<< endl
<< "map.getPane(G_MAP_FLOAT_PANE).appendChild(div);"<< endl
<< "if ( (pixelPosX + div.offsetWidth) > rightEdge ) {"<< endl
<< "div.style.left = (rightEdge - div.offsetWidth - 10) + 'px';"<< endl
<< "}"<< endl
<< "if ( (pixelPosY + div.offsetHeight) > bottomEdge ) {"<< endl
<< "div.style.top = (bottomEdge - div.offsetHeight - 10) + 'px';"<< endl
<< "}"<< endl
<< "});"<< endl
<< "this._map = map;"<< endl
<< "this._div = div;"<< endl
<< "}"<< endl
<< "MapTooltip.prototype.remove = function() {"<< endl
<< "if(this._div != null) {"<< endl
<< "this._div.parentNode.removeChild(this._div);"<< endl
<< "}"<< endl
<< "}"<< endl
<< "MapTooltip.prototype.redraw = function(force) {"<< endl
<< "}"<< endl
<< "</script> "<< endl;
return oss.str();
}

93
src/GoogleMapControl.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 2009 Greg Lonnon (greg.lonnon@gmail.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_GoogleMapControl_h
#define _GC_GoogleMapControl_h
#include <QWidget>
#include <QtWebKit>
#include <string>
#include <iostream>
#include <sstream>
#include <string>
#include "RideFile.h"
#include "MainWindow.h"
class QMouseEvent;
class RideItem;
class MainWindow;
class QColor;
class QVBoxLayout;
class QTabWidget;
class GoogleMapControl : public QWidget
{
Q_OBJECT
private:
MainWindow *main;
QVBoxLayout *layout;
QWebView *view;
MainWindow *parent;
GoogleMapControl(); // default ctor
int range;
std::string CreatePolyLine();
void CreateSubPolyLine(const std::vector<RideFilePoint> &points,
std::ostringstream &oss,
int avgPower);
std::string CreateMapToolTipJavaScript();
std::string CreateIntervalHtml(QHash<QString,RideMetricPtr> &metrics, QStringList &intervalMetrics,
QString &intervalName, bool useMetrics);
std::string CreateMarkers(RideItem *ride);
std::string CreateIntervalMarkers(RideItem *ride);
void loadRide();
std::string CreateMarker(int number, double lat, double lon, std::string &html);
// the web browser is loading a page, do NOT start another load
bool loadingPage;
// the ride has changed, load a new page
bool newRideToLoad;
QColor GetColor(int watts);
// a GPS normalized vectory of ride data points,
// when a GPS unit loses signal it seems to
// put a coordinate close to 180 into the data
std::vector<RideFilePoint> rideData;
// current ride CP
int rideCP;
// current HTML for the ride
std::ostringstream currentPage;
RideItem *current;
public slots:
void rideSelected();
private slots:
void loadStarted();
void loadFinished(bool);
protected:
void createHtml(RideItem *);
void resizeEvent(QResizeEvent *);
public:
GoogleMapControl(MainWindow *);
virtual ~GoogleMapControl() { }
};
#endif

233
src/GpxParser.cpp Normal file
View File

@@ -0,0 +1,233 @@
/*
* Copyright (c) 2010 Greg Lonnon (greg.lonnon@gmail.com) copied from
* TcxParser.cpp
* 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 <QString>
#include <QDebug>
#include "GpxParser.h"
#include "TimeUtils.h"
#include <math.h>
// use stc strtod to bypass Qt toDouble() issues
#include <stdlib.h>
GpxParser::GpxParser (RideFile* rideFile)
: rideFile(rideFile)
{
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
isGarminSmartRecording = settings->value(GC_GARMIN_SMARTRECORD,Qt::Checked);
GarminHWM = settings->value(GC_GARMIN_HWMARK);
if (GarminHWM.isNull() || GarminHWM.toInt() == 0)
GarminHWM.setValue(25); // default to 25 seconds.
distance = 0;
lastLat = lastLon = 0;
alt =0;
lon = 0;
lat = 0;
firstTime = true;
metadata = false;
}
bool GpxParser::startElement( const QString&, const QString&,
const QString& qName,
const QXmlAttributes& qAttributes)
{
buffer.clear();
if(metadata)
return true;
if(qName == "metadata")
{
metadata = true;
}
else if(qName == "trkpt")
{
int i = qAttributes.index("lat");
if(i >= 0)
{
lat = qAttributes.value(i).toDouble();
}
else
{
lat = lastLat;
}
i = qAttributes.index("lon");
if( i >= 0)
{
lon = qAttributes.value(i).toDouble();
}
else
{
lon = lastLon;
}
}
return TRUE;
}
#define PI 3.14159265
inline double toRadians(double degrees)
{
return degrees * 2 * PI / 360;
}
bool
GpxParser::endElement( const QString&, const QString&, const QString& qName)
{
if(qName == "metadata")
{
metadata = false;
}
else if(metadata == true)
{
return true;
}
else if (qName == "time")
{
time = convertToLocalTime(buffer);
if(firstTime)
{
start_time = time;
rideFile->setStartTime(time);
firstTime = false;
}
}
else if (qName == "ele")
{
alt = buffer.toDouble(); // metric
}
else if (qName == "trkpt")
{
if(lastLon == 0)
{
// update the "lasts" and find the next point
last_time = time;
lastLon = lon;
lastLat = lat;
return TRUE;
}
// we need to figure out the distance by using the lon,lat
// using teh haversine formula
double r = 6371;
double dlat = toRadians(lat -lastLat); // convert to radians
double dlon = toRadians(lon - lastLon);
double a = sin(dlat /2) * sin(dlat/2) + cos(lat) * cos(lastLat) * sin(dlon/2) * sin(dlon /2);
double c = 2 * atan2(sqrt(a),sqrt(1-a));
double delta_d = r * c;
if(lastLat != 0)
distance += delta_d;
// compute the elapsed time and distance traveled since the
// last recorded trackpoint
double delta_t = last_time.secsTo(time);
if (delta_d<0)
{
delta_d=0;
}
// compute speed for this trackpoint by dividing the distance
// traveled by the elapsed time. The elapsed time will be 0.0
// for the first trackpoint -- so set speed to 0.0 instead of
// dividing by zero.
double speed = 0.0;
if (delta_t > 0.0)
{
speed=delta_d / delta_t * 3600.0;
}
// Time from beginning of activity
double secs = start_time.secsTo(time);
// Record trackpoint
// for smart recording, the delta_t will not be constant
// average all the calculations based on the previous
// point.
if(rideFile->dataPoints().empty()) {
// first point
rideFile->appendPoint(secs, 0, 0, distance,
speed, 0, 0, alt, lon, lat, 0, 0);
}
else {
// assumption that the change in ride is linear... :)
RideFilePoint *prevPoint = rideFile->dataPoints().back();
double deltaSecs = secs - prevPoint->secs;
double deltaDist = distance - prevPoint->km;
double deltaSpeed = speed - prevPoint->kph;
double deltaAlt = alt - prevPoint->alt;
double deltaLon = lon - prevPoint->lon;
double deltaLat = lat - prevPoint->lat;
// Smart Recording High Water Mark.
if ((isGarminSmartRecording.toInt() == 0) || (deltaSecs == 1) || (deltaSecs >= GarminHWM.toInt())) {
// no smart recording, or delta exceeds HW treshold, just insert the data
rideFile->appendPoint(secs, 0, 0, distance,
speed, 0,0, alt, lon, lat, 0, 0);
}
else {
// smart recording is on and delta is less than GarminHWM seconds.
for(int i = 1; i <= deltaSecs; i++) {
double weight = i/ deltaSecs;
double kph = prevPoint->kph + (deltaSpeed *weight);
// need to make sure speed goes to zero
kph = kph > 0.35 ? kph : 0;
double lat = prevPoint->lat + (deltaLat * weight);
double lon = prevPoint->lon + (deltaLon * weight);
rideFile->appendPoint(
prevPoint->secs + (deltaSecs * weight),
0,
0,
prevPoint->km + (deltaDist * weight),
kph,
0,
0,
prevPoint->alt + (deltaAlt * weight),
lon, // lon
lat, // lat
0,
0);
}
prevPoint = rideFile->dataPoints().back();
}
}
// update the "lasts" and find the next point
last_time = time;
lastLon = lon;
lastLat = lat;
}
return TRUE;
}
bool GpxParser::characters( const QString& str )
{
buffer += str;
return TRUE;
}

68
src/GpxParser.h Normal file
View File

@@ -0,0 +1,68 @@
/*
* Copyright (c) 2010 Greg Lonnon (greg.lonnon@gmail.com) copied from
* TcxParser.cpp
* 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 _TcxParser_h
#define _TcxParser_h
#include "RideFile.h"
#include <QString>
#include <QDateTime>
#include <QXmlDefaultHandler>
#include "Settings.h"
class GpxParser : public QXmlDefaultHandler
{
public:
GpxParser(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:
RideFile* rideFile;
QString buffer;
QVariant isGarminSmartRecording;
QVariant GarminHWM;
QDateTime start_time;
QDateTime last_time;
QDateTime time;
double distance;
double lastLat, lastLon;
double alt;
double lat;
double lon;
// set to false after the first time element is seen (not in metadata)
bool firstTime;
// throw away the metadata, it doesn't look useful
bool metadata;
};
#endif // _TcxParser_h

44
src/GpxRideFile.cpp Normal file
View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2010 Greg Lonnon (greg.lonnon@gmail.com) copied from TcxRideFile.cpp
*
* 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 "GpxRideFile.h"
#include "GpxParser.h"
static int tcxFileReaderRegistered =
RideFileFactory::instance().registerReader(
"gpx", "GGPS Exchange format", new GpxFileReader());
RideFile *GpxFileReader::openRideFile(QFile &file, QStringList &errors) const
{
(void) errors;
RideFile *rideFile = new RideFile();
rideFile->setRecIntSecs(1.0);
rideFile->setDeviceType("GPS Exchange Format");
GpxParser handler(rideFile);
QXmlInputSource source (&file);
QXmlSimpleReader reader;
reader.setContentHandler (&handler);
reader.parse (source);
return rideFile;
}

32
src/GpxRideFile.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2010 Greg Lonnon (greg.lonnon@gmail.com)
*
* 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 _GpxRideFile_h
#define _GpxRideFile_h
#include "RideFile.h"
struct GpxFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
};
#endif // _GpxRideFile_h

View File

@@ -21,9 +21,13 @@
#include "PowerHist.h"
#include "RideFile.h"
#include "RideItem.h"
#include "Settings.h"
#include <QtGui>
#include <assert.h>
#include "Zones.h"
#include "HrZones.h"
HistogramWindow::HistogramWindow(MainWindow *mainWindow) :
QWidget(mainWindow), mainWindow(mainWindow)
{
@@ -49,9 +53,20 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) :
withZerosCheckBox = new QCheckBox;
withZerosCheckBox->setText(tr("With zeros"));
binWidthLayout->addWidget(withZerosCheckBox);
histShadeZones = new QCheckBox;
histShadeZones->setText(tr("Shade zones"));
histShadeZones->setChecked(true);
binWidthLayout->addWidget(histShadeZones);
histParameterCombo = new QComboBox();
binWidthLayout->addWidget(histParameterCombo);
histSumY = new QComboBox();
histSumY->addItem(tr("Absolute Time"));
histSumY->addItem(tr("Percentage Time"));
binWidthLayout->addWidget(histSumY);
powerHist = new PowerHist(mainWindow);
setHistTextValidator();
@@ -73,17 +88,29 @@ HistogramWindow::HistogramWindow(MainWindow *mainWindow) :
this, SLOT(setWithZerosFromCheckBox()));
connect(histParameterCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(setHistSelection(int)));
connect(histShadeZones, SIGNAL(stateChanged(int)),
this, SLOT(setHistSelection(int)));
connect(histSumY, SIGNAL(currentIndexChanged(int)),
this, SLOT(setSumY(int)));
connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
connect(mainWindow, SIGNAL(intervalSelected()), this, SLOT(intervalSelected()));
connect(mainWindow, SIGNAL(zonesChanged()), this, SLOT(zonesChanged()));
connect(mainWindow, SIGNAL(configChanged()), powerHist, SLOT(configChanged()));
}
void
HistogramWindow::rideSelected()
{
if (mainWindow->activeTab() != this)
return;
RideItem *ride = mainWindow->rideItem();
if (!ride)
return;
// get range that applies to this ride
powerRange = mainWindow->zones()->whichRange(ride->dateTime.date());
hrRange = mainWindow->hrZones()->whichRange(ride->dateTime.date());
// set the histogram data
powerHist->setData(ride);
// make sure the histogram has a legal selection
@@ -95,6 +122,8 @@ HistogramWindow::rideSelected()
void
HistogramWindow::intervalSelected()
{
if (mainWindow->activeTab() != this)
return;
RideItem *ride = mainWindow->rideItem();
if (!ride) return;
@@ -105,6 +134,8 @@ HistogramWindow::intervalSelected()
void
HistogramWindow::zonesChanged()
{
if (mainWindow->activeTab() != this)
return;
powerHist->refreshZoneLabels();
powerHist->replot();
}
@@ -125,6 +156,13 @@ HistogramWindow::setlnYHistFromCheckBox()
powerHist->setlnY(! powerHist->islnY());
}
void
HistogramWindow::setSumY(int index)
{
if (index < 0) return; // being destroyed
else powerHist->setSumY(index == 0);
}
void
HistogramWindow::setWithZerosFromCheckBox()
{
@@ -164,22 +202,40 @@ HistogramWindow::setHistTextValidator()
}
void
HistogramWindow::setHistSelection(int id)
HistogramWindow::setHistSelection(int /*id*/)
{
if (id == histWattsShadedID)
powerHist->setSelection(PowerHist::wattsShaded);
else if (id == histWattsUnshadedID)
powerHist->setSelection(PowerHist::wattsUnshaded);
// Set shading first, since the dataseries selection
// below will trigger a redraw, and we need to have
// set the shading beforehand. OK, so we could make
// either change trigger it, but this makes for simpler
// code here and in powerhist.cpp
if (histShadeZones->isChecked()) powerHist->setShading(true);
else powerHist->setShading(false);
// lets get the selection since we are called from
// the checkbox and combobox signal
int id = histParameterCombo->currentIndex();
// Which data series are we plotting?
if (id == histWattsID)
powerHist->setSelection(PowerHist::watts);
else if (id == histWattsZoneID) // we can zone power!
powerHist->setSelection(PowerHist::wattsZone);
else if (id == histNmID)
powerHist->setSelection(PowerHist::nm);
powerHist->setSelection(PowerHist::nm);
else if (id == histHrID)
powerHist->setSelection(PowerHist::hr);
powerHist->setSelection(PowerHist::hr);
else if (id == histHrZoneID) // we can zone HR!
powerHist->setSelection(PowerHist::hrZone);
else if (id == histKphID)
powerHist->setSelection(PowerHist::kph);
powerHist->setSelection(PowerHist::kph);
else if (id == histCadID)
powerHist->setSelection(PowerHist::cad);
powerHist->setSelection(PowerHist::cad);
else
fprintf(stderr, "Illegal id encountered: %d", id);
powerHist->setSelection(PowerHist::watts);
// just in case it gets switch off...
if (!powerHist->islnY()) lnYHistCheckBox->setChecked(false);
setHistBinWidthText();
setHistTextValidator();
@@ -208,23 +264,22 @@ HistogramWindow::setHistWidgets(RideItem *rideItem)
if (ride) {
// we want to retain the present selection
PowerHist::Selection s = powerHist->selection();
PowerHist::Selection s = powerHist->selection();
histParameterCombo->clear();
histWattsShadedID =
histWattsUnshadedID =
histNmID =
histHrID =
histKphID =
histCadID =
-1;
histWattsID = histWattsZoneID =
histNmID = histHrID = histHrZoneID =
histKphID = histCadID = -1;
if (ride->areDataPresent()->watts) {
histWattsShadedID = count ++;
histParameterCombo->addItem(tr("Watts(shaded)"));
histWattsUnshadedID = count ++;
histParameterCombo->addItem(tr("Watts(unshaded)"));
histWattsID = count ++;
histParameterCombo->addItem(tr("Watts"));
if (powerRange != -1) {
histWattsZoneID = count ++;
histParameterCombo->addItem(tr("Watts (by Zone)"));
}
}
if (ride->areDataPresent()->nm) {
histNmID = count ++;
@@ -233,6 +288,10 @@ HistogramWindow::setHistWidgets(RideItem *rideItem)
if (ride->areDataPresent()->hr) {
histHrID = count ++;
histParameterCombo->addItem(tr("Heartrate"));
if (hrRange != -1) {
histHrZoneID = count ++;
histParameterCombo->addItem(tr("Heartrate (by Zone)"));
}
}
if (ride->areDataPresent()->kph) {
histKphID = count ++;
@@ -250,14 +309,16 @@ HistogramWindow::setHistWidgets(RideItem *rideItem)
lnYHistCheckBox->setEnabled(true);
// set widget to proper value
if ((s == PowerHist::wattsShaded) && (histWattsShadedID >= 0))
histParameterCombo->setCurrentIndex(histWattsShadedID);
else if ((s == PowerHist::wattsUnshaded) && (histWattsUnshadedID >= 0))
histParameterCombo->setCurrentIndex(histWattsUnshadedID);
if ((s == PowerHist::watts) && (histWattsID >= 0))
histParameterCombo->setCurrentIndex(histWattsID);
else if ((s == PowerHist::wattsZone) && (histWattsZoneID >= 0))
histParameterCombo->setCurrentIndex(histWattsZoneID);
else if ((s == PowerHist::nm) && (histNmID >= 0))
histParameterCombo->setCurrentIndex(histNmID);
else if ((s == PowerHist::hr) && (histHrID >= 0))
histParameterCombo->setCurrentIndex(histHrID);
else if ((s == PowerHist::hrZone) && (histHrZoneID >= 0))
histParameterCombo->setCurrentIndex(histHrZoneID);
else if ((s == PowerHist::kph) && (histKphID >= 0))
histParameterCombo->setCurrentIndex(histKphID);
else if ((s == PowerHist::cad) && (histCadID >= 0))

View File

@@ -51,6 +51,7 @@ class HistogramWindow : public QWidget
void setlnYHistFromCheckBox();
void setWithZerosFromCheckBox();
void setHistSelection(int id);
void setSumY(int);
protected:
@@ -63,15 +64,20 @@ class HistogramWindow : public QWidget
QLineEdit *binWidthLineEdit;
QCheckBox *lnYHistCheckBox;
QCheckBox *withZerosCheckBox;
QCheckBox *histShadeZones;
QComboBox *histParameterCombo;
QComboBox *histSumY;
int histWattsShadedID;
int histWattsUnshadedID;
int histWattsID;
int histWattsZoneID;
int histNmID;
int histHrID;
int histHrZoneID;
int histKphID;
int histCadID;
int histAltID;
int powerRange, hrRange;
};
#endif // _GC_HistogramWindow_h

222
src/HrTimeInZone.cpp Normal file
View File

@@ -0,0 +1,222 @@
/*
* Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch), Mark Liversedge (liversedge@gmail.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 "RideMetric.h"
#include "BestIntervalDialog.h"
#include "HrZones.h"
#include <math.h>
#include <QApplication>
class HrZoneTime : public RideMetric {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime)
int level;
double seconds;
QList<int> lo;
QList<int> hi;
public:
HrZoneTime() : level(0), seconds(0.0)
{
setType(RideMetric::Total);
setMetricUnits("seconds");
setImperialUnits("seconds");
setPrecision(0);
setConversion(1.0);
}
void setLevel(int level) { this->level=level-1; } // zones start from zero not 1
void compute(const RideFile *ride, const Zones *, int, const HrZones *hrZone, int hrZoneRange,
const QHash<QString,RideMetric*> &)
{
seconds = 0;
// get zone ranges
if (hrZone && hrZoneRange >= 0) {
// iterate and compute
foreach(const RideFilePoint *point, ride->dataPoints()) {
if (hrZone->whichZone(hrZoneRange, point->hr) == level)
seconds += ride->recIntSecs();
}
}
setValue(seconds);
}
bool canAggregate() const { return false; }
void aggregateWith(const RideMetric &) {}
RideMetric *clone() const { return new HrZoneTime(*this); }
};
class HrZoneTime1 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime1)
public:
HrZoneTime1()
{
setLevel(1);
setSymbol("time_in_zone_H1");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H1 Time in Zone");
}
void initialize () {
#endif
setName(tr("H1 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime1(*this); }
};
class HrZoneTime2 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime2)
public:
HrZoneTime2()
{
setLevel(2);
setSymbol("time_in_zone_H2");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H2 Time in Zone");
}
void initialize () {
#endif
setName(tr("H2 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime2(*this); }
};
class HrZoneTime3 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime3)
public:
HrZoneTime3()
{
setLevel(3);
setSymbol("time_in_zone_H3");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H3 Time in Zone");
}
void initialize () {
#endif
setName(tr("H3 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime3(*this); }
};
class HrZoneTime4 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime4)
public:
HrZoneTime4()
{
setLevel(4);
setSymbol("time_in_zone_H4");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H4 Time in Zone");
}
void initialize () {
#endif
setName(tr("H4 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime4(*this); }
};
class HrZoneTime5 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime5)
public:
HrZoneTime5()
{
setLevel(5);
setSymbol("time_in_zone_H5");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H5 Time in Zone");
}
void initialize () {
#endif
setName(tr("H5 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime5(*this); }
};
class HrZoneTime6 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime6)
public:
HrZoneTime6()
{
setLevel(6);
setSymbol("time_in_zone_H6");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H6 Time in Zone");
}
void initialize () {
#endif
setName(tr("H6 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime6(*this); }
};
class HrZoneTime7 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime7)
public:
HrZoneTime7()
{
setLevel(7);
setSymbol("time_in_zone_H7");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H7 Time in Zone");
}
void initialize () {
#endif
setName(tr("H7 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime7(*this); }
};
class HrZoneTime8 : public HrZoneTime {
Q_DECLARE_TR_FUNCTIONS(HrZoneTime8)
public:
HrZoneTime8()
{
setLevel(8);
setSymbol("time_in_zone_H8");
#ifdef ENABLE_METRICS_TRANSLATION
setInternalName("H8 Time in Zone");
}
void initialize () {
#endif
setName(tr("H8 Time in Zone"));
}
RideMetric *clone() const { return new HrZoneTime8(*this); }
};
static bool addAllHrZones() {
RideMetricFactory::instance().addMetric(HrZoneTime1());
RideMetricFactory::instance().addMetric(HrZoneTime2());
RideMetricFactory::instance().addMetric(HrZoneTime3());
RideMetricFactory::instance().addMetric(HrZoneTime4());
RideMetricFactory::instance().addMetric(HrZoneTime5());
RideMetricFactory::instance().addMetric(HrZoneTime6());
RideMetricFactory::instance().addMetric(HrZoneTime7());
RideMetricFactory::instance().addMetric(HrZoneTime8());
return true;
}
static bool allHrZonesAdded = addAllHrZones();

880
src/HrZones.cpp Normal file
View File

@@ -0,0 +1,880 @@
/*
* Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch)
*
* 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 <QMessageBox>
#include "HrZones.h"
#include "Colors.h"
#include "TimeUtils.h"
#include <QtGui>
#include <QtAlgorithms>
#include <qcolor.h>
#include <assert.h>
#include <math.h>
#include <boost/crc.hpp>
// the infinity endpoints are indicated with extreme date ranges
// but not zero dates so we can edit and compare them
static const QDate date_zero(1900, 01, 01);
static const QDate date_infinity(9999,12,31);
// initialize default static zone parameters
void HrZones::initializeZoneParameters()
{
static int initial_zone_default[] = {
0, 68, 83, 94, 105
};
static double initial_zone_default_trimp[] = {
0.9, 1.1, 1.2, 2.0, 5.0
};
static const QString initial_zone_default_desc[] = {
tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"),
tr("VO2Max")
};
static const char *initial_zone_default_name[] = {
"Z1", "Z2", "Z3", "Z4", "Z5"
};
static int initial_nzones_default =
sizeof(initial_zone_default) /
sizeof(initial_zone_default[0]);
scheme.zone_default.clear();
scheme.zone_default_is_pct.clear();
scheme.zone_default_desc.clear();
scheme.zone_default_name.clear();
scheme.zone_default_trimp.clear();
scheme.nzones_default = 0;
scheme.nzones_default = initial_nzones_default;
for (int z = 0; z < scheme.nzones_default; z ++) {
scheme.zone_default.append(initial_zone_default[z]);
scheme.zone_default_is_pct.append(true);
scheme.zone_default_name.append(QString(initial_zone_default_name[z]));
scheme.zone_default_desc.append(initial_zone_default_desc[z]);
scheme.zone_default_trimp.append(initial_zone_default_trimp[z]);
}
}
// read zone file, allowing for zones with or without end dates
bool HrZones::read(QFile &file)
{
defaults_from_user = false;
scheme.zone_default.clear();
scheme.zone_default_is_pct.clear();
scheme.zone_default_name.clear();
scheme.zone_default_desc.clear();
scheme.zone_default_trimp.clear();
scheme.nzones_default = 0;
ranges.clear();
// set up possible warning dialog
warning = QString();
int warning_lines = 0;
const int max_warning_lines = 100;
// macro to append lines to the warning
#define append_to_warning(s) \
if (warning_lines < max_warning_lines) \
warning.append(s); \
else if (warning_lines == max_warning_lines) \
warning.append("...\n"); \
warning_lines ++;
// read using text mode takes care of end-lines
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
err = "can't open file";
return false;
}
QTextStream fileStream(&file);
QRegExp commentrx("\\s*#.*$");
QRegExp blankrx("^[ \t]*$");
QRegExp rangerx[] = {
QRegExp("^\\s*(?:from\\s+)?" // optional "from"
"((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
"\\s*([,:]?\\s*(LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)}
"\\s*([,:]?\\s*(RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)}
"\\s*([,:]?\\s*(MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)}
"\\s*:?\\s*$", // optional :
Qt::CaseInsensitive),
QRegExp("^\\s*(?:from\\s+)?" // optional "from"
"((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date
"\\s+(?:until|to|-)\\s+" // until
"((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date
"\\s*:?,?\\s*((LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)}
"\\s*:?,?\\s*((RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)}
"\\s*:?,?\\s*((MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)}
"\\s*:?\\s*$", // optional :
Qt::CaseInsensitive)
};
QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*"
"(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+(\\.\\d+)?)\\s*)?$",
Qt::CaseInsensitive);//
QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$",
Qt::CaseInsensitive);
int lineno = 0;
// the current range in the file
// ZoneRange *range = NULL;
bool in_range = false;
QDate begin = date_zero, end = date_infinity;
int lt = 0;
int restHr = 0;
int maxHr = 0;
QList<HrZoneInfo> zoneInfos;
// true if zone defaults are found in the file (then we need to write them)
bool zones_are_defaults = false;
while (! fileStream.atEnd() ) {
++lineno;
QString line = fileStream.readLine();
int pos = commentrx.indexIn(line, 0);
if (pos != -1)
line = line.left(pos);
if (blankrx.indexIn(line, 0) == 0)
goto next_line;
// check for default zone range definition (may be followed by hr zone definitions)
if (zonedefaultsx.indexIn(line, 0) != -1) {
zones_are_defaults = true;
// defaults are allowed only at the beginning of the file
if (ranges.size()) {
err = "HR Zone defaults must be specified at head of hr.zones file";
return false;
}
// only one set of defaults is allowed
if (scheme.nzones_default) {
err = "Only one set of zone defaults may be specified in hr.zones file";
return false;
}
goto next_line;
}
// check for range specification (may be followed by zone definitions)
for (int r=0; r<2; r++) {
if (rangerx[r].indexIn(line, 0) != -1) {
if (in_range) {
// if zones are empty, then generate them
HrZoneRange range(begin, end, lt, restHr, maxHr);
range.zones = zoneInfos;
if (range.zones.empty()) {
if (range.lt > 0) setHrZonesFromLT(range);
else {
err = tr("line %1: read new range without reading "
"any zones for previous one").arg(lineno);
file.close();
return false;
}
} else {
qSort(range.zones);
}
ranges.append(range);
}
in_range = true;
zones_are_defaults = false;
zoneInfos.clear();
// process the beginning date
if (rangerx[r].cap(1) == "BEGIN")
begin = date_zero;
else {
begin = QDate(rangerx[r].cap(2).toInt(),
rangerx[r].cap(3).toInt(),
rangerx[r].cap(4).toInt());
}
// process an end date, if any, else it is null
if (rangerx[r].cap(5) == "END") end = date_infinity;
else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() ||
rangerx[r].cap(8).toInt()) {
end = QDate(rangerx[r].cap(6).toInt(),
rangerx[r].cap(7).toInt(),
rangerx[r].cap(8).toInt());
} else {
end = QDate();
}
// set up the range, capturing LT if it's specified
// range = new ZoneRange(begin, end);
int nLT = (r ? 11 : 7);
if (rangerx[r].numCaptures() >= (nLT)) lt = rangerx[r].cap(nLT).toInt();
else lt = 0;
int nRestHr = (r ? 14 : 10);
if (rangerx[r].numCaptures() >= (nRestHr)) restHr = rangerx[r].cap(nRestHr).toInt();
else restHr = 0;
int nMaxHr = (r ? 17 : 13);
if (rangerx[r].numCaptures() >= (nRestHr)) maxHr = rangerx[r].cap(nMaxHr).toInt();
else maxHr = 0;
// bleck
goto next_line;
}
}
// check for zone definition
if (zonerx.indexIn(line, 0) != -1) {
if (! (in_range || zones_are_defaults)) {
err = tr("line %1: read zone without "
"preceeding date range").arg(lineno);
file.close();
return false;
}
int lo = zonerx.cap(3).toInt();
double trimp = zonerx.cap(5).toDouble();
// allow for zone specified as % of LT
bool lo_is_pct = false;
if (zonerx.cap(4) == "%") {
if (zones_are_defaults) lo_is_pct = true;
else if (lt > 0) lo = int(lo * lt / 100);
else {
err = tr("attempt to set zone based on % of "
"LT without setting LT in line number %1.\n").
arg(lineno);
file.close();
return false;
}
}
int hi = -1; // signal an undefined number
double tr = zonerx.cap(5).toDouble();
if (zones_are_defaults) {
scheme.nzones_default ++;
scheme.zone_default_is_pct.append(lo_is_pct);
scheme.zone_default.append(lo);
scheme.zone_default_name.append(zonerx.cap(1));
scheme.zone_default_desc.append(zonerx.cap(2));
scheme.zone_default_trimp.append(trimp);
defaults_from_user = true;
}
else {
HrZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi, tr);
zoneInfos.append(zone);
}
}
next_line: {}
}
if (in_range) {
HrZoneRange range(begin, end, lt, restHr, maxHr);
range.zones = zoneInfos;
if (range.zones.empty()) {
if (range.lt > 0)
setHrZonesFromLT(range);
else {
err = tr("file ended without reading any zones for last range");
file.close();
return false;
}
}
else {
qSort(range.zones);
}
ranges.append(range);
}
file.close();
// sort the ranges
qSort(ranges);
// set the default zones if not in file
if (!scheme.nzones_default) {
// do we have a zone which is explicitly set?
for (int i=0; i<ranges.count(); i++) {
if (ranges[i].hrZonesSetFromLT == false) {
// set the defaults using this one!
scheme.nzones_default = ranges[i].zones.count();
for (int j=0; j<scheme.nzones_default; j++) {
scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].lt) * 100.00);
scheme.zone_default_is_pct.append(true);
scheme.zone_default_name.append(ranges[i].zones[j].name);
scheme.zone_default_desc.append(ranges[i].zones[j].desc);
scheme.zone_default_trimp.append(ranges[i].zones[j].trimp);
}
}
}
// still not set then reset to defaults as usual
if (!scheme.nzones_default) initializeZoneParameters();
}
// resolve undefined endpoints in ranges and zones
for (int nr = 0; nr < ranges.size(); nr ++) {
// clean up gaps or overlaps in zone ranges
if (ranges[nr].end.isNull())
ranges[nr].end =
(nr < ranges.size() - 1) ?
ranges[nr + 1].begin :
date_infinity;
else if ((nr < ranges.size() - 1) &&
(ranges[nr + 1].begin != ranges[nr].end)) {
append_to_warning(tr("Setting end date of range %1 "
"to start date of range %2.\n").
arg(nr + 1).
arg(nr + 2)
);
ranges[nr].end = ranges[nr + 1].begin;
}
else if ((nr == ranges.size() - 1) &&
(ranges[nr].end < QDate::currentDate())) {
append_to_warning(tr("Extending final range %1 to infinite "
"to include present date.\n").arg(nr + 1));
ranges[nr].end = date_infinity;
}
if (ranges[nr].lt <= 0) {
err = tr("LT must be greater than zero in zone "
"range %1 of hr.zones").arg(nr + 1);
return false;
}
if (ranges[nr].zones.size()) {
// check that the first zone starts with zero
ranges[nr].zones[0].lo = 0;
// resolve zone end powers
for (int nz = 0; nz < ranges[nr].zones.size(); nz ++) {
if (ranges[nr].zones[nz].hi == -1)
ranges[nr].zones[nz].hi =
(nz < ranges[nr].zones.size() - 1) ?
ranges[nr].zones[nz + 1].lo :
INT_MAX;
else if ((nz < ranges[nr].zones.size() - 1) &&
(ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) {
if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) {
append_to_warning(tr("Range %1: matching top of zone %2 "
"(%3) to bottom of zone %4 (%5).\n").
arg(nr + 1).
arg(ranges[nr].zones[nz].name).
arg(ranges[nr].zones[nz].hi).
arg(ranges[nr].zones[nz + 1].name).
arg(ranges[nr].zones[nz + 1].lo)
);
}
ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo;
} else if ((nz == ranges[nr].zones.size() - 1) &&
(ranges[nr].zones[nz].hi < INT_MAX)) {
append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n").
arg(nr + 1).
arg(ranges[nr].zones[nz].name).
arg(ranges[nr].zones[nz].hi)
);
ranges[nr].zones[nz].hi = INT_MAX;
}
}
}
}
// mark zones as modified so pages which depend on zones can be updated
modificationTime = QDateTime::currentDateTime();
return true;
}
// note empty dates are treated as automatic matches for begin or
// end of range
int HrZones::whichRange(const QDate &date) const
{
for (int rnum = 0; rnum < ranges.size(); ++rnum) {
const HrZoneRange &range = ranges[rnum];
if (((date >= range.begin) || (range.begin.isNull())) &&
((date < range.end) || (range.end.isNull())))
return rnum;
}
return -1;
}
int HrZones::numZones(int rnum) const
{
assert(rnum < ranges.size());
return ranges[rnum].zones.size();
}
int HrZones::whichZone(int rnum, double value) const
{
assert(rnum < ranges.size());
const HrZoneRange &range = ranges[rnum];
for (int j = 0; j < range.zones.size(); ++j) {
const HrZoneInfo &info = range.zones[j];
// note: the "end" of range is actually in the next zone
if ((value >= info.lo) && (value < info.hi))
return j;
}
// if we got here either it is negative, nan, inf or way high
if (value < 0 || isnan(value)) return 0;
else return range.zones.size()-1;
}
void HrZones::zoneInfo(int rnum, int znum,
QString &name, QString &description,
int &low, int &high, double &trimp) const
{
assert(rnum < ranges.size());
const HrZoneRange &range = ranges[rnum];
assert(znum < range.zones.size());
const HrZoneInfo &zone = range.zones[znum];
name = zone.name;
description = zone.desc;
low = zone.lo;
high = zone.hi;
trimp= zone.trimp;
}
int HrZones::getLT(int rnum) const
{
assert(rnum < ranges.size());
return ranges[rnum].lt;
}
void HrZones::setLT(int rnum, int lt)
{
ranges[rnum].lt = lt;
modificationTime = QDateTime::currentDateTime();
}
// generate a list of zones from LT
int HrZones::lowsFromLT(QList <int> *lows, int lt) const {
lows->clear();
for (int z = 0; z < scheme.nzones_default; z++)
lows->append(scheme.zone_default_is_pct[z] ?
scheme.zone_default[z] * lt / 100 : scheme.zone_default[z]);
return scheme.nzones_default;
}
int HrZones::getRestHr(int rnum) const
{
assert(rnum < ranges.size());
return ranges[rnum].restHr;
}
void HrZones::setRestHr(int rnum, int restHr)
{
ranges[rnum].restHr = restHr;
modificationTime = QDateTime::currentDateTime();
}
int HrZones::getMaxHr(int rnum) const
{
assert(rnum < ranges.size());
return ranges[rnum].maxHr;
}
void HrZones::setMaxHr(int rnum, int maxHr)
{
ranges[rnum].maxHr = maxHr;
modificationTime = QDateTime::currentDateTime();
}
// access the zone name
QString HrZones::getDefaultZoneName(int z) const {
return scheme.zone_default_name[z];
}
// access the zone description
QString HrZones::getDefaultZoneDesc(int z) const {
return scheme.zone_default_desc[z];
}
// set the zones from the LT value (the cp variable)
void HrZones::setHrZonesFromLT(HrZoneRange &range) {
range.zones.clear();
if (scheme.nzones_default == 0)
initializeZoneParameters();
for (int i = 0; i < scheme.nzones_default; i++) {
int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.lt / 100 : scheme.zone_default[i];
int hi = lo;
double trimp = scheme.zone_default_trimp[i];
HrZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi, trimp);
range.zones.append(zone);
}
// sort the zones (some may be pct, others absolute, so zones need to be sorted,
// rather than the defaults
qSort(range.zones);
// set zone end dates
for (int i = 0; i < range.zones.size(); i ++)
range.zones[i].hi =
(i < scheme.nzones_default - 1) ?
range.zones[i + 1].lo :
INT_MAX;
// mark that the zones were set from LT, so if zones are subsequently
// written, only LT is saved
range.hrZonesSetFromLT = true;
}
void HrZones::setHrZonesFromLT(int rnum) {
assert((rnum >= 0) && (rnum < ranges.size()));
setHrZonesFromLT(ranges[rnum]);
}
// return the list of starting values of zones for a given range
QList <int> HrZones::getZoneLows(int rnum) const {
if (rnum >= ranges.size())
return QList <int>();
const HrZoneRange &range = ranges[rnum];
QList <int> return_values;
for (int i = 0; i < range.zones.size(); i ++)
return_values.append(ranges[rnum].zones[i].lo);
return return_values;
}
// return the list of ending values of zones for a given range
QList <int> HrZones::getZoneHighs(int rnum) const {
if (rnum >= ranges.size())
return QList <int>();
const HrZoneRange &range = ranges[rnum];
QList <int> return_values;
for (int i = 0; i < range.zones.size(); i ++)
return_values.append(ranges[rnum].zones[i].hi);
return return_values;
}
// return the list of zone names
QList <QString> HrZones::getZoneNames(int rnum) const {
if (rnum >= ranges.size())
return QList <QString>();
const HrZoneRange &range = ranges[rnum];
QList <QString> return_values;
for (int i = 0; i < range.zones.size(); i ++)
return_values.append(ranges[rnum].zones[i].name);
return return_values;
}
// return the list of zone trimp coef
QList <double> HrZones::getZoneTrimps(int rnum) const {
if (rnum >= ranges.size())
return QList <double>();
const HrZoneRange &range = ranges[rnum];
QList <double> return_values;
for (int i = 0; i < range.zones.size(); i ++)
return_values.append(ranges[rnum].zones[i].trimp);
return return_values;
}
QString HrZones::summarize(int rnum, QVector<double> &time_in_zone) const
{
assert(rnum < ranges.size());
const HrZoneRange &range = ranges[rnum];
assert(time_in_zone.size() == range.zones.size());
QString summary;
if(range.lt > 0){
summary += "<table align=\"center\" width=\"70%\" border=\"0\">";
summary += "<tr><td align=\"center\">";
summary += tr("Threshold (bpm): %1").arg(range.lt);
summary += "</td></tr></table>";
}
summary += "<table align=\"center\" width=\"70%\" ";
summary += "border=\"0\">";
summary += "<tr>";
summary += tr("<td align=\"center\">Zone</td>");
summary += tr("<td align=\"center\">Description</td>");
summary += tr("<td align=\"center\">Low (bpm)</td>");
summary += tr("<td align=\"center\">High (bpm)</td>");
summary += tr("<td align=\"center\">Time</td>");
summary += tr("<td align=\"center\">%</td>");
summary += "</tr>";
QColor color = QApplication::palette().alternateBase().color();
color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value());
double duration = 0;
foreach(double v, time_in_zone) { duration += v; }
for (int zone = 0; zone < time_in_zone.size(); ++zone) {
if (time_in_zone[zone] > 0.0) {
QString name, desc;
int lo, hi;
double trimp;
zoneInfo(rnum, zone, name, desc, lo, hi, trimp);
if (zone % 2 == 0)
summary += "<tr bgcolor='" + color.name() + "'>";
else
summary += "<tr>";
summary += QString("<td align=\"center\">%1</td>").arg(name);
summary += QString("<td align=\"center\">%1</td>").arg(desc);
summary += QString("<td align=\"center\">%1</td>").arg(lo);
if (hi == INT_MAX)
summary += "<td align=\"center\">MAX</td>";
else
summary += QString("<td align=\"center\">%1</td>").arg(hi);
summary += QString("<td align=\"center\">%1</td>")
.arg(time_to_string((unsigned) round(time_in_zone[zone])));
summary += QString("<td align=\"center\">%1</td>")
.arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0);
summary += "</tr>";
}
}
summary += "</table>";
return summary;
}
#define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */
void HrZones::write(QDir home)
{
QString strzones;
// always write the defaults (config pane can adjust)
strzones += QString("DEFAULTS:\n");
for (int z = 0 ; z < scheme.nzones_default; z ++)
strzones += QString("%1,%2,%3%4,%5\n").
arg(scheme.zone_default_name[z]).
arg(scheme.zone_default_desc[z]).
arg(scheme.zone_default[z]).
arg(scheme.zone_default_is_pct[z]?"%":"").
arg(scheme.zone_default_trimp[z]);
strzones += QString("\n");
for (int i = 0; i < ranges.size(); i++) {
int lt = getLT(i);
int restHr = getRestHr(i);
int maxHr = getMaxHr(i);
// print header for range
// note this explicitly sets the first and last ranges such that all time is spanned
// note: BEGIN is not needed anymore
// since it becomes Jan 01 1900
strzones += QString("%1: LT=%2, RestHr=%3, MaxHr=%4").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(lt).arg(restHr).arg(maxHr);
strzones += QString("\n");
// step through and print the zones if they've been explicitly set
if (! ranges[i].hrZonesSetFromLT) {
for (int j = 0; j < ranges[i].zones.size(); j ++) {
const HrZoneInfo &zi = ranges[i].zones[j];
strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.trimp);
}
strzones += QString("\n");
}
}
QFile file(home.absolutePath() + "/hr.zones");
if (file.open(QFile::WriteOnly))
{
QTextStream stream(&file);
stream << strzones;
file.close();
}
}
void HrZones::addHrZoneRange(QDate _start, QDate _end, int _lt, int _restHr, int _maxHr)
{
ranges.append(HrZoneRange(_start, _end, _lt, _restHr, _maxHr));
}
// insert a new zone range using the current scheme
// return the range number
int HrZones::addHrZoneRange(QDate _start, int _lt, int _restHr, int _maxHr)
{
int rnum;
// where to add this range?
for(rnum=0; rnum < ranges.count(); rnum++) if (ranges[rnum].begin > _start) break;
// at the end ?
if (rnum == ranges.count()) ranges.append(HrZoneRange(_start, date_infinity, _lt, _restHr, _maxHr));
else ranges.insert(rnum, HrZoneRange(_start, ranges[rnum].begin, _lt, _restHr, _maxHr));
// modify previous end date
if (rnum) ranges[rnum-1].end = _start;
// set zones from LT
if (_lt > 0) {
setLT(rnum, _lt);
setHrZonesFromLT(rnum);
}
return rnum;
}
void HrZones::addHrZoneRange()
{
ranges.append(HrZoneRange(date_zero, date_infinity));
}
void HrZones::setEndDate(int rnum, QDate endDate)
{
ranges[rnum].end = endDate;
modificationTime = QDateTime::currentDateTime();
}
void HrZones::setStartDate(int rnum, QDate startDate)
{
ranges[rnum].begin = startDate;
modificationTime = QDateTime::currentDateTime();
}
QDate HrZones::getStartDate(int rnum) const
{
assert(rnum >= 0);
return ranges[rnum].begin;
}
QString HrZones::getStartDateString(int rnum) const
{
assert(rnum >= 0);
QDate d = ranges[rnum].begin;
return (d.isNull() ? "BEGIN" : d.toString());
}
QDate HrZones::getEndDate(int rnum) const
{
assert(rnum >= 0);
return ranges[rnum].end;
}
QString HrZones::getEndDateString(int rnum) const
{
assert(rnum >= 0);
QDate d = ranges[rnum].end;
return (d.isNull() ? "END" : d.toString());
}
int HrZones::getRangeSize() const
{
return ranges.size();
}
// generate a zone color with a specific number of zones
QColor hrZoneColor(int z, int) {
switch(z) {
case 0 : return GColor(CHZONE1); break;
case 1 : return GColor(CHZONE2); break;
case 2 : return GColor(CHZONE3); break;
case 3 : return GColor(CHZONE4); break;
case 4 : return GColor(CHZONE5); break;
case 5 : return GColor(CHZONE6); break;
case 6 : return GColor(CHZONE7); break;
case 7 : return GColor(CHZONE8); break;
case 8 : return GColor(CHZONE9); break;
case 9 : return GColor(CHZONE10); break;
default: return QColor(128,128,128); break;
}
}
// delete a range, extend an adjacent (prior if available, otherwise next)
// range to cover the same time period, then return the number of the new range
// covering the date range of the deleted range or -1 if none left
int HrZones::deleteRange(int rnum) {
// check bounds - silently fail, don't assert
assert (rnum < ranges.count() && rnum >= 0);
// extend the previous range to the end of this range
// but only if we have a previous range
if (rnum > 0) setEndDate(rnum-1, getEndDate(rnum));
// delete this range then
ranges.removeAt(rnum);
return rnum-1;
}
// insert a new range starting at the given date extending to the end of the zone currently
// containing that date. If the start date of that zone is prior to the specified start
// date, then that zone range is shorted.
int HrZones::insertRangeAtDate(QDate date, int lt) {
assert(date.isValid());
int rnum;
if (ranges.empty()) {
addHrZoneRange();
rnum = 0;
}
else {
rnum = whichRange(date);
assert(rnum >= 0);
QDate date1 = getStartDate(rnum);
// if the old range has dates before the specified, then truncate
// the old range and shift up the existing ranges
if (date > date1) {
QDate endDate = getEndDate(rnum);
setEndDate(rnum, date);
ranges.insert(++ rnum, HrZoneRange(date, endDate));
}
}
if (lt > 0) {
setLT(rnum, lt);
setHrZonesFromLT(rnum);
}
return rnum;
}
unsigned long
HrZones::getFingerprint() const
{
boost::crc_optimal<16, 0x1021, 0xFFFF, 0, false, false> CRC;
for (int i=0; i<ranges.size(); i++) {
// from
int x = ranges[i].begin.toJulianDay();
CRC.process_bytes(&x, sizeof(int));
// to
x = ranges[i].end.toJulianDay();
CRC.process_bytes(&x, sizeof(int));
// CP
x = ranges[i].lt;
CRC.process_bytes(&x, sizeof(int));
// each zone definition (manual edit/default changed)
for (int j=0; j<ranges[i].zones.count(); j++) {
x = ranges[i].zones[j].lo;
CRC.process_bytes(&x, sizeof(int));
}
}
return CRC.checksum();
}

212
src/HrZones.h Normal file
View File

@@ -0,0 +1,212 @@
/*
* Copyright (c) 2010 Damien Grauser (Damien.Grauser@pev-geneve.ch), 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 _HrZones_h
#define _HrZones_h
#include <QtCore>
// A zone "scheme" defines how power zones
// are calculated as a percentage of LT
// The default is to use Coggan percentages
// but this can be overriden in hr.zones
struct HrZoneScheme {
QList <int> zone_default;
QList <bool> zone_default_is_pct;
QList <QString> zone_default_name;
QList <QString> zone_default_desc;
QList <double> zone_default_trimp;
int nzones_default;
};
// A zone "info" defines a *single zone*
// in absolute watts terms e.g.
// "L4" "Threshold"
struct HrZoneInfo {
QString name, desc;
int lo, hi;
double trimp;
HrZoneInfo(const QString &n, const QString &d, int l, int h, double t) :
name(n), desc(d), lo(l), hi(h), trimp(t) {}
// used by qSort()
bool operator< (HrZoneInfo right) const {
return ((lo < right.lo) || ((lo == right.lo) && (hi < right.hi)));
}
};
// A zone "range" defines the power zones
// that are active for a *specific date period*
// e.g. between 01/01/2008 and 01/04/2008
// my LT was 170 and I chose to setup
// 5 zoneinfos from Active Recovery through
// to VO2Max
struct HrZoneRange {
QDate begin, end;
int lt;
int restHr;
int maxHr;
QList<HrZoneInfo> zones;
bool hrZonesSetFromLT;
HrZoneRange(const QDate &b, const QDate &e) :
begin(b), end(e), lt(0), hrZonesSetFromLT(false) {}
HrZoneRange(const QDate &b, const QDate &e, int _lt, int _restHr, int _maxHr) :
begin(b), end(e), lt(_lt), restHr(_restHr), maxHr(_maxHr), hrZonesSetFromLT(false) {}
// used by qSort()
bool operator< (HrZoneRange right) const {
return (((! right.begin.isNull()) &&
(begin.isNull() || begin < right.begin )) ||
((begin == right.begin) && (! end.isNull()) &&
( right.end.isNull() || end < right.end )));
}
};
class HrZones : public QObject
{
Q_OBJECT
private:
// Scheme
bool defaults_from_user;
HrZoneScheme scheme;
// LT History
QList<HrZoneRange> ranges;
// utility
QString err, warning;
void setHrZonesFromLT(HrZoneRange &range);
public:
HrZones() : defaults_from_user(false) {
initializeZoneParameters();
}
//
// Zone settings - Scheme (& default scheme)
//
HrZoneScheme getScheme() const { return scheme; }
void setScheme(HrZoneScheme x) { scheme = x; }
// get defaults from the current scheme
QString getDefaultZoneName(int z) const;
QString getDefaultZoneDesc(int z) const;
// set zone parameters to either user-specified defaults
// or to defaults using Coggan's coefficients
void initializeZoneParameters();
//
// Zone history - Ranges
//
// How many ranges in our history
int getRangeSize() const;
// Add ranges
void addHrZoneRange(QDate _start, QDate _end, int _lt, int _restHr, int _maxHr);
int addHrZoneRange(QDate _start, int _lt, int _restHr, int _maxHr);
void addHrZoneRange();
// insert a range from the given date to the end date of the range
// presently including the date
int insertRangeAtDate(QDate date, int lt = 0);
// Get / Set ZoneRange details
HrZoneRange getHrZoneRange(int rnum) { return ranges[rnum]; }
void setHrZoneRange(int rnum, HrZoneRange x) { ranges[rnum] = x; }
// get and set LT for a given range
int getLT(int rnum) const;
void setLT(int rnum, int cp);
// get and set Rest Hr for a given range
int getRestHr(int rnum) const;
void setRestHr(int rnum, int restHr);
// get and set LT for a given range
int getMaxHr(int rnum) const;
void setMaxHr(int rnum, int maxHr);
// calculate and then set zoneinfo for a given range
void setHrZonesFromLT(int rnum);
// delete the range rnum, and adjust dates on adjacent zone; return
// the range number of the range extended to cover the deleted zone
int deleteRange(const int rnum);
//
// read and write hr.zones
//
bool read(QFile &file);
void write(QDir home);
const QString &errorString() const { return err; }
const QString &warningString() const { return warning; }
//
// Typical APIs to get details of ranges and zones
//
// which range is active for a particular date
int whichRange(const QDate &date) const;
// which zone is the power value in for a given range
int whichZone(int range, double value) const;
// how many zones are there for a given range
int numZones(int range) const;
// get zoneinfo for a given range and zone
void zoneInfo(int range, int zone,
QString &name, QString &description,
int &low, int &high, double &trimp) const;
QString summarize(int rnum, QVector<double> &time_in_zone) const;
// get all highs/lows for zones (plot shading uses these)
int lowsFromLT(QList <int> *lows, int LT) const;
QList <int> getZoneLows(int rnum) const;
QList <int> getZoneHighs(int rnum) const;
QList <double> getZoneTrimps(int rnum) const;
QList <QString> getZoneNames(int rnum) const;
// get/set range start and end date
QDate getStartDate(int rnum) const;
QDate getEndDate(int rnum) const;
QString getStartDateString(int rnum) const;
QString getEndDateString(int rnum) const;
void setEndDate(int rnum, QDate date);
void setStartDate(int rnum, QDate date);
// When was this last updated?
QDateTime modificationTime;
// calculate a CRC for the zones data - used to see if zones
// data is changed since last referenced in Metric code
// could also be used in Configuration pages (later)
unsigned long getFingerprint() const;
};
QColor hrZoneColor(int zone, int num_zones);
#endif // _HrZones_h

38
src/JsonRideFile.h Normal file
View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2009 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 _JsonRideFile_h
#define _JsonRideFile_h
#include "RideFile.h"
#include <stdio.h>
#include <algorithm> // for std::sort
#include <QDomDocument>
#include <QVector>
#include <assert.h>
#include <QDebug>
#define DATETIME_FORMAT "yyyy/MM/dd hh:mm:ss' UTC'"
struct JsonFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
virtual void writeRideFile(const RideFile *ride, QFile &file) const;
};
#endif // _JsonRideFile_h

73
src/JsonRideFile.l Normal file
View File

@@ -0,0 +1,73 @@
%{
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "JsonRideFile.h"
// we use stdio for reading from FILE *JsonRideFilein
// because thats what lex likes to do, and since we're
// reading files that seems ok anyway
#include <stdio.h>
// The parser defines the token values for us
// so lets include them before declaring the
// token patterns
#include "JsonRideFile_yacc.h"/* generated by the scanner */
// the options below tell flex to no bother with
// yywrap since we only ever read a single file
// anyway. And yyunput() isn't needed for our
// parser, we read in one pass with no swanky
// interactions
%}
%option noyywrap
%option nounput
%%
\"RIDE\" return RIDE;
\"STARTTIME\" return STARTTIME;
\"RECINTSECS\" return RECINTSECS;
\"DEVICETYPE\" return DEVICETYPE;
\"IDENTIFIER\" return IDENTIFIER;
\"OVERRIDES\" return OVERRIDES;
\"TAGS\" return TAGS;
\"INTERVALS\" return INTERVALS;
\"NAME\" return NAME;
\"START\" return START;
\"STOP\" return STOP;
\"SAMPLES\" return SAMPLES;
\"SECS\" return SECS;
\"KM\" return KM;
\"WATTS\" return WATTS;
\"NM\" return NM;
\"CAD\" return CAD;
\"KPH\" return KPH;
\"HR\" return HR;
\"ALT\" return ALTITUDE; // ALT clashes with qtnamespace.h:46
\"LAT\" return LAT;
\"LON\" return LON;
\"HEADWIND\" return HEADWIND;
\"SLOPE\" return SLOPE;
\"TEMP\" return TEMP;
[-+]?[0-9]+ return INTEGER;
[-+]?[0-9]+e-[0-9]+ return FLOAT;
[-+]?[0-9]+\.[-e0-9]* return FLOAT;
\"([^\"]|\\\")*\" return STRING; /* contains non-quotes or escaped-quotes */
[ \n\t\r] ; /* we just ignore whitespace */
. return JsonRideFiletext[0]; /* any other character, typically :, { or } */
%%

417
src/JsonRideFile.y Normal file
View File

@@ -0,0 +1,417 @@
%{
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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
*/
// This grammar should work with yacc and bison, but has
// only been tested with bison. In addition, since qmake
// uses the -p flag to rename all the yy functions to
// enable multiple grammars in a single executable you
// should make sure you use the very latest bison since it
// has been known to be problematic in the past. It is
// know to work well with bison v2.4.1.
//
// To make the grammar readable I have placed the code
// for each nterm at column 40, this source file is best
// edited / viewed in an editor which is at least 120
// columns wide (e.g. vi in xterm of 120x40)
//
//
// The grammar is specific to the RideFile format serialised
// in writeRideFile below, this is NOT a generic json parser.
#include "JsonRideFile.h"
// Set during parser processing, using same
// naming conventions as yacc/lex -p
static RideFile *JsonRide;
// term state data is held in these variables
static RideFilePoint JsonPoint;
static RideFileInterval JsonInterval;
static QString JsonString,
JsonTagKey, JsonTagValue,
JsonOverName, JsonOverKey, JsonOverValue;
static double JsonNumber;
static QStringList JsonRideFileerrors;
static QMap <QString, QString> JsonOverrides;
// Standard yacc/lex variables / functions
extern int JsonRideFilelex(); // the lexer aka yylex()
extern void JsonRideFilerestart (FILE *input_file); // the lexer file restart aka yyrestart()
extern FILE *JsonRideFilein; // used by the lexer aka yyin
extern char *JsonRideFiletext; // set by the lexer aka yytext
void JsonRideFileerror(const char *error) // used by parser aka yyerror()
{ JsonRideFileerrors << error; }
//
// Utility functions
//
// Escape special characters (JSON compliance)
static QString protect(const QString string)
{
QString s = string;
s.replace("\\", "\\\\"); // backslash
s.replace("\"", "\\\""); // quote
s.replace("\t", "\\t"); // tab
s.replace("\n", "\\n"); // newline
s.replace("\r", "\\r"); // carriage-return
s.replace("\b", "\\b"); // backspace
s.replace("\f", "\\f"); // formfeed
s.replace("/", "\\/"); // solidus
return s;
}
// Un-Escape special characters (JSON compliance)
static QString unprotect(const QString string)
{
QString string2 = QString::fromLocal8Bit(string.toAscii().data());
// this is a lexer string so it will be enclosed
// in quotes. Lets strip those first
QString s = string2.mid(1,string2.length()-2);
// now un-escape the control characters
s.replace("\\t", "\t"); // tab
s.replace("\\n", "\n"); // newline
s.replace("\\r", "\r"); // carriage-return
s.replace("\\b", "\b"); // backspace
s.replace("\\f", "\f"); // formfeed
s.replace("\\/", "/"); // solidus
s.replace("\\\"", "\""); // quote
s.replace("\\\\", "\\"); // backslash
return s;
}
%}
%token STRING INTEGER FLOAT
%token RIDE STARTTIME RECINTSECS DEVICETYPE IDENTIFIER
%token OVERRIDES
%token TAGS INTERVALS NAME START STOP
%token SAMPLES SECS KM WATTS NM CAD KPH HR ALTITUDE LAT LON HEADWIND SLOPE TEMP
%start document
%%
/* We allow a .json file to be encapsulated within optional braces */
document: '{' ride_list '}'
| ride_list
;
/* multiple rides in a single file are supported, rides will be joined */
ride_list:
ride
| ride_list ',' ride
;
ride: RIDE ':' '{' rideelement_list '}' ;
rideelement_list: rideelement_list ',' rideelement
| rideelement
;
rideelement: starttime
| recordint
| devicetype
| identifier
| overrides
| tags
| intervals
| samples
;
/*
* First class variables
*/
starttime: STARTTIME ':' string {
QDateTime aslocal = QDateTime::fromString(JsonString, DATETIME_FORMAT);
QDateTime asUTC = QDateTime(aslocal.date(), aslocal.time(), Qt::UTC);
JsonRide->setStartTime(asUTC.toLocalTime());
}
recordint: RECINTSECS ':' number { JsonRide->setRecIntSecs(JsonNumber); }
devicetype: DEVICETYPE ':' string { JsonRide->setDeviceType(JsonString); }
identifier: IDENTIFIER ':' string { /* not supported in v2.1 */ }
/*
* Metric Overrides
*/
overrides: OVERRIDES ':' '[' overrides_list ']' ;
overrides_list: override | overrides_list ',' override ;
override: '{' override_name ':' override_values '}' { JsonRide->metricOverrides.insert(JsonOverName, JsonOverrides);
JsonOverrides.clear();
}
override_name: string { JsonOverName = JsonString; }
override_values: '{' override_value_list '}';
override_value_list: override_value | override_value_list ',' override_value ;
override_value: override_key ':' override_value { JsonOverrides.insert(JsonOverKey, JsonOverValue); }
override_key : string { JsonOverKey = JsonString; }
override_value : string { JsonOverValue = JsonString; }
/*
* Ride metadata tags
*/
tags: TAGS ':' '{' tags_list '}'
tags_list: tag | tags_list ',' tag ;
tag: tag_key ':' tag_value { JsonRide->setTag(JsonTagKey, JsonTagValue); }
tag_key : string { JsonTagKey = JsonString; }
tag_value : string { JsonTagValue = JsonString; }
/*
* Intervals
*/
intervals: INTERVALS ':' '[' interval_list ']' ;
interval_list: interval | interval_list ',' interval ;
interval: '{' NAME ':' string ',' { JsonInterval.name = JsonString; }
START ':' number ',' { JsonInterval.start = JsonNumber; }
STOP ':' number { JsonInterval.stop = JsonNumber; }
'}'
{ JsonRide->addInterval(JsonInterval.start,
JsonInterval.stop,
JsonInterval.name);
JsonInterval = RideFileInterval();
}
/*
* Ride datapoints
*/
samples: SAMPLES ':' '[' sample_list ']' ;
sample_list: sample | sample_list ',' sample ;
sample: '{' series_list '}' { JsonRide->appendPoint(JsonPoint.secs, JsonPoint.cad,
JsonPoint.hr, JsonPoint.km, JsonPoint.kph,
JsonPoint.nm, JsonPoint.watts, JsonPoint.alt,
JsonPoint.lon, JsonPoint.lat,
JsonPoint.headwind, JsonPoint.interval);
JsonPoint = RideFilePoint();
}
series_list: series | series_list ',' series ;
series: SECS ':' number { JsonPoint.secs = JsonNumber; }
| KM ':' number { JsonPoint.km = JsonNumber; }
| WATTS ':' number { JsonPoint.watts = JsonNumber; }
| NM ':' number { JsonPoint.nm = JsonNumber; }
| CAD ':' number { JsonPoint.cad = JsonNumber; }
| KPH ':' number { JsonPoint.kph = JsonNumber; }
| HR ':' number { JsonPoint.hr = JsonNumber; }
| ALTITUDE ':' number { JsonPoint.alt = JsonNumber; }
| LAT ':' number { JsonPoint.lat = JsonNumber; }
| LON ':' number { JsonPoint.lon = JsonNumber; }
| HEADWIND ':' number { JsonPoint.headwind = JsonNumber; }
| SLOPE ':' number { }
| TEMP ':' number { }
;
/*
* Primitives
*/
number: INTEGER { JsonNumber = QString(JsonRideFiletext).toInt(); }
| FLOAT { JsonNumber = QString(JsonRideFiletext).toDouble(); }
;
string: STRING { JsonString = unprotect(JsonRideFiletext); }
;
%%
static int jsonFileReaderRegistered =
RideFileFactory::instance().registerReader(
"json", "GoldenCheetah Json Format", new JsonFileReader());
RideFile *
JsonFileReader::openRideFile(QFile &file, QStringList &errors) const
{
// jsonRide is local and static, used in the parser
// JsonRideFilein is the FILE * used by the lexer
JsonRideFilein = fopen(file.fileName().toLatin1(), "r");
if (JsonRideFilein == NULL) {
errors << "unable to open file" + file.fileName();
}
// inform the parser/lexer we have a new file
JsonRideFilerestart(JsonRideFilein);
// setup
JsonRide = new RideFile;
JsonRideFileerrors.clear();
// set to non-zero if you want to
// to debug the yyparse() state machine
// sending state transitions to stderr
//yydebug = 0;
// parse it
JsonRideFileparse();
// release the file handle
fclose(JsonRideFilein);
// Only get errors so fail if we have any
if (errors.count()) {
errors << JsonRideFileerrors;
delete JsonRide;
return NULL;
} else return JsonRide;
}
// Writes valid .json (validated at www.jsonlint.com)
void
JsonFileReader::writeRideFile(const RideFile *ride, QFile &file) const
{
// can we open the file for writing?
if (!file.open(QIODevice::WriteOnly)) return;
// truncate existing
file.resize(0);
// setup streamer
QTextStream out(&file);
// start of document and ride
out << "{\n\t\"RIDE\":{\n";
// first class variables
out << "\t\t\"STARTTIME\":\"" << protect(ride->startTime().toUTC().toString(DATETIME_FORMAT)) << "\",\n";
out << "\t\t\"RECINTSECS\":" << ride->recIntSecs() << ",\n";
out << "\t\t\"DEVICETYPE\":\"" << protect(ride->deviceType()) << "\",\n";
// not available in v2.1 -- out << "\t\t\"IDENTIFIER\":\"" << protect(ride->id()) << "\"";
//
// OVERRIDES
//
bool nonblanks = false; // if an override has been deselected it may be blank
// so we only output the OVERRIDES section if we find an
// override whilst iterating over the QMap
if (ride->metricOverrides.count()) {
QMap<QString,QMap<QString, QString> >::const_iterator k;
for (k=ride->metricOverrides.constBegin(); k != ride->metricOverrides.constEnd(); k++) {
// may not contain anything
if (k.value().isEmpty()) continue;
if (nonblanks == false) {
out << ",\n\t\t\"OVERRIDES\":[\n";
nonblanks = true;
}
// begin of overrides
out << "\t\t\t{ \"" << k.key() << "\":{ ";
// key/value pairs
QMap<QString, QString>::const_iterator j;
for (j=k.value().constBegin(); j != k.value().constEnd(); j++) {
// comma separated
out << "\"" << j.key() << "\":\"" << j.value() << "\"";
if (j+1 != k.value().constEnd()) out << ", ";
}
if (k+1 != ride->metricOverrides.constEnd()) out << " }},\n";
else out << " }}\n";
}
if (nonblanks == true) {
// end of the overrides
out << "\t\t]";
}
}
//
// TAGS
//
if (ride->tags().count()) {
out << ",\n\t\t\"TAGS\":{\n";
QMap<QString,QString>::const_iterator i;
for (i=ride->tags().constBegin(); i != ride->tags().constEnd(); i++) {
out << "\t\t\t\"" << i.key() << "\":\"" << protect(i.value()) << "\"";
if (i+1 != ride->tags().constEnd()) out << ",\n";
else out << "\n";
}
// end of the tags
out << "\t\t}";
}
//
// INTERVALS
//
if (!ride->intervals().empty()) {
out << ",\n\t\t\"INTERVALS\":[\n";
bool first = true;
foreach (RideFileInterval i, ride->intervals()) {
if (first) first=false;
else out << ",\n";
out << "\t\t\t{ ";
out << "\"NAME\":\"" << protect(i.name) << "\"";
out << ", \"START\": " << QString("%1").arg(i.start);
out << ", \"STOP\": " << QString("%1").arg(i.stop) << " }";
}
out <<"\n\t\t]";
}
//
// SAMPLES
//
if (ride->dataPoints().count()) {
out << ",\n\t\t\"SAMPLES\":[\n";
bool first = true;
foreach (RideFilePoint *p, ride->dataPoints()) {
if (first) first=false;
else out << ",\n";
out << "\t\t\t{ ";
// always store time
out << "\"SECS\":" << QString("%1").arg(p->secs);
if (ride->areDataPresent()->km) out << ", \"KM\":" << QString("%1").arg(p->km);
if (ride->areDataPresent()->watts) out << ", \"WATTS\":" << QString("%1").arg(p->watts);
if (ride->areDataPresent()->nm) out << ", \"NM\":" << QString("%1").arg(p->nm);
if (ride->areDataPresent()->cad) out << ", \"CAD\":" << QString("%1").arg(p->cad);
if (ride->areDataPresent()->kph) out << ", \"KPH\":" << QString("%1").arg(p->kph);
if (ride->areDataPresent()->hr) out << ", \"HR\":" << QString("%1").arg(p->hr);
if (ride->areDataPresent()->alt) out << ", \"ALT\":" << QString("%1").arg(p->alt);
if (ride->areDataPresent()->lat)
out << ", \"LAT\":" << QString("%1").arg(p->lat, 0, 'g', 11);
if (ride->areDataPresent()->lon)
out << ", \"LON\":" << QString("%1").arg(p->lon, 0, 'g', 11);
if (ride->areDataPresent()->headwind) out << ", \"HEADWIND\":" << QString("%1").arg(p->headwind);
// sample points in here!
out << " }";
}
out <<"\n\t\t]";
}
// end of ride and document
out << "\n\t}\n}\n";
// close
file.close();
}

305
src/KmlRideFile.cpp Normal file
View File

@@ -0,0 +1,305 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "KmlRideFile.h"
#include <QDebug>
#include <time.h>
#include <iostream>
#include <string>
#include "boost/scoped_ptr.hpp"
#include "kml/base/date_time.h"
#include "kml/base/expat_parser.h"
#include "kml/base/file.h"
#include "kml/base/vec3.h"
#include "kml/convenience/convenience.h"
#include "kml/convenience/gpx_trk_pt_handler.h"
#include "kml/dom.h"
#include "kml/dom/kml_ptr.h"
// majority of code swiped from the libkml example gpx2kml.cc
using kmlbase::ExpatParser;
using kmlbase::DateTime;
using kmlbase::Vec3;
using kmlbase::Attributes;
using kmldom::ContainerPtr;
using kmldom::FolderPtr;
using kmldom::IconStylePtr;
using kmldom::IconStyleIconPtr;
using kmldom::KmlFactory;
using kmldom::KmlPtr;
using kmldom::LabelStylePtr;
using kmldom::ListStylePtr;
using kmldom::PairPtr;
using kmldom::PointPtr;
using kmldom::SchemaPtr;
using kmldom::ExtendedDataPtr;
using kmldom::SchemaDataPtr;
using kmldom::GxSimpleArrayFieldPtr;
using kmldom::GxSimpleArrayDataPtr;
using kmldom::GxMultiTrackPtr;
using kmldom::GxTrackPtr;
using kmldom::PlacemarkPtr;
using kmldom::TimeStampPtr;
using kmldom::StylePtr;
using kmldom::StyleMapPtr;
//
// Utility functions
//
static const char kDotIcon[] =
"http://maps.google.com/mapfiles/kml/shapes/shaded_dot.png";
static ExtendedDataPtr CreateExtendedData() {
KmlFactory* kml_factory = KmlFactory::GetFactory();
ExtendedDataPtr ed = kml_factory->CreateExtendedData();
return ed;
}
static SchemaDataPtr CreateSchemaData(string name) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
SchemaDataPtr sd = kml_factory->CreateSchemaData();
sd->set_schemaurl("#" + name);
return sd;
}
static GxSimpleArrayDataPtr CreateGxSimpleArrayData(string name) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
GxSimpleArrayDataPtr sa = kml_factory->CreateGxSimpleArrayData();
sa->set_name(name);
return sa;
}
static PlacemarkPtr CreateGxTrackPlacemark(string name, GxTrackPtr tracks) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
PlacemarkPtr placemark = kml_factory->CreatePlacemark();
placemark->set_name(name);
placemark->set_id(name);
placemark->set_geometry(tracks);
return placemark;
}
static GxTrackPtr CreateGxTrack(string name) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
GxTrackPtr track = kml_factory->CreateGxTrack();
track->set_id(name);
return track;
}
static SchemaPtr CreateSchema(string name) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
SchemaPtr schema = kml_factory->CreateSchema();
schema->set_id(name);
schema->set_name(name);
return schema;
}
static GxSimpleArrayFieldPtr CreateGxSimpleArrayField(string name) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
GxSimpleArrayFieldPtr field = kml_factory->CreateGxSimpleArrayField();
field->set_type("float");
field->set_name(name);
field->set_displayname(name);
return field;
}
static IconStylePtr CreateIconStyle(double scale) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
IconStyleIconPtr icon = kml_factory->CreateIconStyleIcon();
icon->set_href(kDotIcon);
IconStylePtr icon_style = kml_factory->CreateIconStyle();
icon_style->set_icon(icon);
icon_style->set_scale(scale);
return icon_style;
}
static LabelStylePtr CreateLabelStyle(double scale) {
LabelStylePtr label_style = KmlFactory::GetFactory()->CreateLabelStyle();
label_style->set_scale(scale);
return label_style;
}
static PairPtr CreatePair(int style_state, double icon_scale) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
PairPtr pair = kml_factory->CreatePair();
pair->set_key(style_state);
StylePtr style = kml_factory->CreateStyle();
style->set_iconstyle(CreateIconStyle(icon_scale));
// Hide the label in normal style state, visible in highlight.
style->set_labelstyle(CreateLabelStyle(
style_state == kmldom::STYLESTATE_NORMAL ? 0 : 1 ));
pair->set_styleselector(style);
return pair;
}
static StylePtr CreateRadioFolder(const char* id) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
ListStylePtr list_style = kml_factory->CreateListStyle();
list_style->set_listitemtype(kmldom::LISTITEMTYPE_RADIOFOLDER);
StylePtr style = kml_factory->CreateStyle();
style->set_liststyle(list_style);
style->set_id(id);
return style;
}
static StyleMapPtr CreateStyleMap(const char* id) {
KmlFactory* kml_factory = KmlFactory::GetFactory();
StyleMapPtr style_map = kml_factory->CreateStyleMap();
style_map->set_id(id);
style_map->add_pair(CreatePair(kmldom::STYLESTATE_NORMAL, 0.1));
style_map->add_pair(CreatePair(kmldom::STYLESTATE_HIGHLIGHT, 0.3));
return style_map;
}
//
// Serialise the ride
//
bool
KmlFileReader::writeRideFile(const RideFile * ride, QFile &file) const
{
// Create a new DOM document and setup styles et al
kmldom::KmlFactory* kml_factory = kmldom::KmlFactory::GetFactory();
kmldom::DocumentPtr document = kml_factory->CreateDocument();
const char* kRadioFolderId = "radio-folder-style";
document->add_styleselector(CreateRadioFolder(kRadioFolderId));
document->set_styleurl(std::string("#") + kRadioFolderId);
const char* kStyleMapId = "style-map";
document->add_styleselector(CreateStyleMap(kStyleMapId));
document->set_name("Golden Cheetah");
// add the schema elements for each data series
SchemaPtr schemadef = CreateSchema("schema");
// gx:SimpleArrayField ...
if (ride->areDataPresent()->cad)
schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("cadence"));
if (ride->areDataPresent()->hr)
schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("heartrate"));
if (ride->areDataPresent()->watts)
schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("power"));
if (ride->areDataPresent()->nm)
schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("torque"));
if (ride->areDataPresent()->headwind)
schemadef->add_gx_simplearrayfield(CreateGxSimpleArrayField("headwind"));
document->add_schema(schemadef);
// setup trip folder (shown on lhs of google earth
FolderPtr folder = kmldom::KmlFactory::GetFactory()->CreateFolder();
folder->set_name("Bike Rides");
document->add_feature(folder);
// Create a track for the entire ride
GxTrackPtr track = CreateGxTrack("Entire Ride");
PlacemarkPtr placemark = CreateGxTrackPlacemark(QString("Bike %1")
.arg(ride->startTime().toString()).toStdString(), track);
folder->add_feature(placemark);
//
// Basic Data -- Lat/Lon/Alt and Timestamp
//
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
// lots of arsing around with dates XXX clean this up
QDateTime timestamp(ride->startTime().addSecs(datapoint->secs));
string stdctimestamp = timestamp.toString(Qt::ISODate).toStdString() + "Z"; //<< 'Z' fixes crash!
kmlbase::DateTime *when = kmlbase::DateTime::Create(stdctimestamp.data());
if (datapoint->lat && datapoint->lon) track->add_when(when->GetXsdDateTime());
}
// <when> loop through the entire ride
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
if (datapoint->lat && datapoint->lon) track->add_gx_coord(kmlbase::Vec3(datapoint->lon, datapoint->lat, datapoint->alt));
}
//
// Extended Data -- cadence, heartrate, power, torque, headwind
//
ExtendedDataPtr extended = CreateExtendedData();
track->set_extendeddata(extended);
SchemaDataPtr schema = CreateSchemaData("schema");
extended->add_schemadata(schema);
// power
if (ride->areDataPresent()->watts) {
GxSimpleArrayDataPtr power = CreateGxSimpleArrayData("power");
schema->add_gx_simplearraydata(power);
// now create a GxSimpleArrayData
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
if (datapoint->lat && datapoint->lon) power->add_gx_value(QString("%1").arg(datapoint->watts).toStdString());
}
}
// cadence
if (ride->areDataPresent()->cad) {
GxSimpleArrayDataPtr cadence = CreateGxSimpleArrayData("cadence");
schema->add_gx_simplearraydata(cadence);
// now create a GxSimpleArrayData
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
if (datapoint->lat && datapoint->lon) cadence->add_gx_value(QString("%1").arg(datapoint->cad).toStdString());
}
}
// heartrate
if (ride->areDataPresent()->hr) {
GxSimpleArrayDataPtr heartrate = CreateGxSimpleArrayData("heartrate");
schema->add_gx_simplearraydata(heartrate);
// now create a GxSimpleArrayData
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
if (datapoint->lat && datapoint->lon) heartrate->add_gx_value(QString("%1").arg(datapoint->hr).toStdString());
}
}
// torque
if (ride->areDataPresent()->nm) {
GxSimpleArrayDataPtr torque = CreateGxSimpleArrayData("torque");
schema->add_gx_simplearraydata(torque);
// now create a GxSimpleArrayData
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
if (datapoint->lat && datapoint->lon) torque->add_gx_value(QString("%1").arg(datapoint->nm).toStdString());
}
}
// headwind
if (ride->areDataPresent()->headwind) {
GxSimpleArrayDataPtr headwind = CreateGxSimpleArrayData("headwind");
schema->add_gx_simplearraydata(headwind);
// now create a GxSimpleArrayData
foreach (RideFilePoint *datapoint, ride->dataPoints()) {
if (datapoint->lat && datapoint->lon) headwind->add_gx_value(QString("%1").arg(datapoint->headwind).toStdString());
}
}
// Create KML document
kmldom::KmlPtr kml = kml_factory->CreateKml();
kml->set_feature(document);
// make sure the google extensions are added in!
kmlbase::Attributes gxxmlns22;
gxxmlns22.SetValue("gx", "http://www.google.com/kml/ext/2.2");
kml->MergeXmlns(gxxmlns22);
// Serialize
if (!file.open(QIODevice::WriteOnly)) return(false);
file.write(kmldom::SerializePretty(kml).data());
file.close();
return(true);
}

29
src/KmlRideFile.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 _KmlRideFile_h
#define _KmlRideFile_h
#include "RideFile.h"
struct KmlFileReader : public RideFileReader {
virtual RideFile *openRideFile(QFile &, QStringList &) const { return NULL; } // does not support reading
virtual bool writeRideFile(const RideFile *ride, QFile &file) const;
};
#endif // _KmlRideFile_h

111
src/LTMCanvasPicker.cpp Normal file
View File

@@ -0,0 +1,111 @@
// code borrowed from event_filter qwt examples
// and modified for GC LTM purposes
#include <qapplication.h>
#include <qevent.h>
#include <qwhatsthis.h>
#include <qpainter.h>
#include <qwt_plot.h>
#include <qwt_symbol.h>
#include <qwt_scale_map.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_curve.h>
#include "LTMCanvasPicker.h"
#include <QDebug>
LTMCanvasPicker::LTMCanvasPicker(QwtPlot *plot):
QObject(plot),
d_selectedCurve(NULL),
d_selectedPoint(-1)
{
QwtPlotCanvas *canvas = plot->canvas();
canvas->installEventFilter(this);
// We want the focus, but no focus rect. The
canvas->setFocusPolicy(Qt::StrongFocus);
canvas->setFocusIndicator(QwtPlotCanvas::ItemFocusIndicator);
canvas->setFocus();
}
bool LTMCanvasPicker::event(QEvent *e)
{
if ( e->type() == QEvent::User )
{
//showCursor(true);
return true;
}
return QObject::event(e);
}
bool LTMCanvasPicker::eventFilter(QObject *object, QEvent *e)
{
if ( object != (QObject *)plot()->canvas() )
return false;
switch(e->type())
{
default:
QApplication::postEvent(this, new QEvent(QEvent::User));
break;
case QEvent::MouseButtonPress:
select(((QMouseEvent *)e)->pos(), true);
break;
case QEvent::MouseMove:
select(((QMouseEvent *)e)->pos(), false);
break;
}
return QObject::eventFilter(object, e);
}
// Select the point at a position. If there is no point
// deselect the selected point
void LTMCanvasPicker::select(const QPoint &pos, bool clicked)
{
QwtPlotCurve *curve = NULL;
double dist = 10e10;
int index = -1;
const QwtPlotItemList& itmList = plot()->itemList();
for ( QwtPlotItemIterator it = itmList.begin();
it != itmList.end(); ++it )
{
if ( (*it)->rtti() == QwtPlotItem::Rtti_PlotCurve )
{
QwtPlotCurve *c = (QwtPlotCurve*)(*it);
double d = -1;
int idx = c->closestPoint(pos, &d);
if ( d != -1 && d < dist )
{
curve = c;
index = idx;
dist = d;
}
}
}
d_selectedCurve = NULL;
d_selectedPoint = -1;
if ( curve && dist < 10 ) // 10 pixels tolerance
{
// picked one
d_selectedCurve = curve;
d_selectedPoint = index;
if (clicked)
pointClicked(curve, index); // emit
else
pointHover(curve, index); // emit
} else {
// didn't
if (clicked)
pointClicked(NULL, -1); // emit
else
pointHover(NULL, -1); // emit
}
}

34
src/LTMCanvasPicker.h Normal file
View File

@@ -0,0 +1,34 @@
// code stolen from the event_filter qwt example
// and modified for GC LTM
#ifndef GC_LTMCanvasPicker_H
#define GC_LTMCanvasPicker_H 1
#include <qobject.h>
class QPoint;
class QCustomEvent;
class QwtPlot;
class QwtPlotCurve;
class LTMCanvasPicker: public QObject
{
Q_OBJECT
public:
LTMCanvasPicker(QwtPlot *plot);
virtual bool eventFilter(QObject *, QEvent *);
virtual bool event(QEvent *);
signals:
void pointClicked(QwtPlotCurve *, int);
void pointHover(QwtPlotCurve *, int);
private:
void select(const QPoint &, bool);
QwtPlot *plot() { return (QwtPlot *)parent(); }
const QwtPlot *plot() const { return (QwtPlot *)parent(); }
QwtPlotCurve *d_selectedCurve;
int d_selectedPoint;
};
#endif

297
src/LTMChartParser.cpp Normal file
View File

@@ -0,0 +1,297 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 "LTMChartParser.h"
#include "LTMSettings.h"
#include "LTMTool.h"
#include <QDate>
#include <QDebug>
#include <assert.h>
// local helper functions to convert Qwt enums to ints and back
static int curveToInt(QwtPlotCurve::CurveStyle x)
{
switch (x) {
case QwtPlotCurve::NoCurve : return 0;
case QwtPlotCurve::Lines : return 1;
case QwtPlotCurve::Sticks : return 2;
case QwtPlotCurve::Steps : return 3;
case QwtPlotCurve::Dots : return 4;
default : return 100;
}
}
static QwtPlotCurve::CurveStyle intToCurve(int x)
{
switch (x) {
default:
case 0 : return QwtPlotCurve::NoCurve;
case 1 : return QwtPlotCurve::Lines;
case 2 : return QwtPlotCurve::Sticks;
case 3 : return QwtPlotCurve::Steps;
case 4 : return QwtPlotCurve::Dots;
case 100: return QwtPlotCurve::UserCurve;
}
}
static int symbolToInt(QwtSymbol::Style x)
{
switch (x) {
default:
case QwtSymbol::NoSymbol: return -1;
case QwtSymbol::Ellipse: return 0;
case QwtSymbol::Rect: return 1;
case QwtSymbol::Diamond: return 2;
case QwtSymbol::Triangle: return 3;
case QwtSymbol::DTriangle: return 4;
case QwtSymbol::UTriangle: return 5;
case QwtSymbol::LTriangle: return 6;
case QwtSymbol::RTriangle: return 7;
case QwtSymbol::Cross: return 8;
case QwtSymbol::XCross: return 9;
case QwtSymbol::HLine: return 10;
case QwtSymbol::VLine: return 11;
case QwtSymbol::Star1: return 12;
case QwtSymbol::Star2: return 13;
case QwtSymbol::Hexagon: return 14;
case QwtSymbol::StyleCnt: return 15;
}
}
static QwtSymbol::Style intToSymbol(int x)
{
switch (x) {
default:
case -1: return QwtSymbol::NoSymbol;
case 0 : return QwtSymbol::Ellipse;
case 1 : return QwtSymbol::Rect;
case 2 : return QwtSymbol::Diamond;
case 3 : return QwtSymbol::Triangle;
case 4 : return QwtSymbol::DTriangle;
case 5 : return QwtSymbol::UTriangle;
case 6 : return QwtSymbol::LTriangle;
case 7 : return QwtSymbol::RTriangle;
case 8 : return QwtSymbol::Cross;
case 9 : return QwtSymbol::XCross;
case 10 : return QwtSymbol::HLine;
case 11 : return QwtSymbol::VLine;
case 12 : return QwtSymbol::Star1;
case 13 : return QwtSymbol::Star2;
case 14 : return QwtSymbol::Hexagon;
case 15 : return QwtSymbol::StyleCnt;
}
}
bool LTMChartParser::startDocument()
{
buffer.clear();
return TRUE;
}
static QString unprotect(QString buffer)
{
// get local TM character code
QTextEdit trademark("&#8482;"); // process html encoding of(TM)
QString tm = trademark.toPlainText();
// remove quotes
QString t = buffer.trimmed();
QString s = t.mid(1,t.length()-2);
// replace html (TM) with local TM character
s.replace( "&#8482;", tm );
// html special chars are automatically handled
// XXX other special characters will not work
// cross-platform but will work locally, so not a biggie
// i.e. if thedefault charts.xml has a special character
// in it it should be added here
return s;
}
// to see the format of the charts.xml file, look at the serialize()
// function at the bottom of this source file.
bool LTMChartParser::endElement( const QString&, const QString&, const QString &qName )
{
//
// Single Attribute elements
//
if(qName == "chartname") setting.name = unprotect(buffer);
else if(qName == "metricname") metric.symbol = buffer.trimmed();
else if(qName == "metricdesc") metric.name = unprotect(buffer);
else if(qName == "metricuname") metric.uname = unprotect(buffer);
else if(qName == "metricuunits") metric.uunits = unprotect(buffer);
else if(qName == "metricbaseline") metric.baseline = buffer.trimmed().toDouble();
else if(qName == "metricsmooth") metric.smooth = buffer.trimmed().toInt();
else if(qName == "metrictrend") metric.trend = buffer.trimmed().toInt();
else if(qName == "metrictopn") metric.topN = buffer.trimmed().toInt();
else if(qName == "metriccurve") metric.curveStyle = intToCurve(buffer.trimmed().toInt());
else if(qName == "metricsymbol") metric.symbolStyle = intToSymbol(buffer.trimmed().toInt());
else if(qName == "metricpencolor") {
// the r,g,b values are in red="xx",green="xx" and blue="xx" attributes
// of this element and captured in startelement below
metric.penColor = QColor(red,green,blue);
}
else if(qName == "metricpenalpha") metric.penAlpha = buffer.trimmed().toInt();
else if(qName == "metricpenwidth") metric.penWidth = buffer.trimmed().toInt();
else if(qName == "metricpenstyle") metric.penStyle = buffer.trimmed().toInt();
else if(qName == "metricbrushcolor") {
// the r,g,b values are in red="xx",green="xx" and blue="xx" attributes
// of this element and captured in startelement below
metric.brushColor = QColor(red,green,blue);
} else if(qName == "metricbrushalpha") metric.penAlpha = buffer.trimmed().toInt();
//
// Complex Elements
//
else if(qName == "metric") // <metric></metric> block
setting.metrics.append(metric);
else if (qName == "LTM-chart") // <LTM-chart></LTM-chart> block
settings.append(setting);
else if (qName == "charts") { // <charts></charts> block top-level
} // do nothing for now
return TRUE;
}
bool LTMChartParser::startElement( const QString&, const QString&, const QString &name, const QXmlAttributes &attrs )
{
buffer.clear();
if(name == "charts")
; // do nothing for now
else if (name == "LTM-chart")
setting = LTMSettings();
else if (name == "metric")
metric = MetricDetail();
else if (name == "metricpencolor" || name == "metricbrushcolor") {
// red="x" green="x" blue="x" attributes for pen/brush color
for(int i=0; i<attrs.count(); i++) {
if (attrs.qName(i) == "red") red=attrs.value(i).toInt();
if (attrs.qName(i) == "green") green=attrs.value(i).toInt();
if (attrs.qName(i) == "blue") blue=attrs.value(i).toInt();
}
}
return TRUE;
}
bool LTMChartParser::characters( const QString& str )
{
buffer += str;
return TRUE;
}
QList<LTMSettings>
LTMChartParser::getSettings()
{
return settings;
}
bool LTMChartParser::endDocument()
{
return TRUE;
}
// static helper to protect special xml characters
// ideally we would use XMLwriter to do this but
// the file format is trivial and this implementation
// is easier to follow and modify... for now.
static QString xmlprotect(QString string)
{
QTextEdit trademark("&#8482;"); // process html encoding of(TM)
QString tm = trademark.toPlainText();
QString s = string;
s.replace( tm, "&#8482;" );
s.replace( "&", "&amp;" );
s.replace( ">", "&gt;" );
s.replace( "<", "&lt;" );
s.replace( "\"", "&quot;" );
s.replace( "\'", "&apos;" );
return s;
}
//
// Write out the charts.xml file
//
void
LTMChartParser::serialize(QString filename, QList<LTMSettings> charts)
{
// open file - truncate contents
QFile file(filename);
file.open(QFile::WriteOnly);
file.resize(0);
QTextStream out(&file);
// Character set encoding added to support international characters in names
out.setCodec("UTF-8");
out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
// begin document
out << "<charts>\n";
// write out to file
foreach (LTMSettings chart, charts) {
// chart name
out<<QString("\t<LTM-chart>\n\t\t<chartname>\"%1\"</chartname>\n").arg(xmlprotect(chart.name));
// all the metrics
foreach (MetricDetail metric, chart.metrics) {
out<<QString("\t\t<metric>\n");
out<<QString("\t\t\t<metricdesc>\"%1\"</metricdesc>\n").arg(xmlprotect(metric.name));
out<<QString("\t\t\t<metricname>%1</metricname>\n").arg(metric.symbol);
out<<QString("\t\t\t<metricuname>\"%1\"</metricuname>\n").arg(xmlprotect(metric.uname));
out<<QString("\t\t\t<metricuunits>\"%1\"</metricuunits>\n").arg(xmlprotect(metric.uunits));
// SMOOTH, TREND, TOPN
out<<QString("\t\t\t<metricsmooth>%1</metricsmooth>\n").arg(metric.smooth);
out<<QString("\t\t\t<metrictrend>%1</metrictrend>\n").arg(metric.trend);
out<<QString("\t\t\t<metrictopn>%1</metrictopn>\n").arg(metric.topN);
out<<QString("\t\t\t<metricbaseline>%1</metricbaseline>\n").arg(metric.baseline);
// CURVE, SYMBOL
out<<QString("\t\t\t<metriccurve>%1</metriccurve>\n").arg(curveToInt(metric.curveStyle));
out<<QString("\t\t\t<metricsymbol>%1</metricsymbol>\n").arg(symbolToInt(metric.symbolStyle));
// PEN
out<<QString("\t\t\t<metricpencolor red=\"%1\" green=\"%3\" blue=\"%4\"></metricpencolor>\n")
.arg(metric.penColor.red())
.arg(metric.penColor.green())
.arg(metric.penColor.blue());
out<<QString("\t\t\t<metricpenalpha>%1</metricpenalpha>\n").arg(metric.penAlpha);
out<<QString("\t\t\t<metricpenwidth>%1</metricpenwidth>\n").arg(metric.penWidth);
out<<QString("\t\t\t<metricpenstyle>%1</metricpenstyle>\n").arg(metric.penStyle);
// BRUSH
out<<QString("\t\t\t<metricbrushcolor red=\"%1\" green=\"%3\" blue=\"%4\"></metricbrushcolor>\n")
.arg(metric.brushColor.red())
.arg(metric.brushColor.green())
.arg(metric.brushColor.blue());
out<<QString("\t\t\t<metricbrushalpha>%1</metricbrushalpha>\n").arg(metric.brushAlpha);
out<<QString("\t\t</metric>\n");
}
out<<QString("\t</LTM-chart>\n");
}
// end document
out << "</charts>\n";
// close file
file.close();
}

47
src/LTMChartParser.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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_LTMChartParser_h
#define _GC_LTMChartParser_h 1
#include <QXmlDefaultHandler>
#include "LTMSettings.h"
#include "LTMTool.h"
class LTMChartParser : public QXmlDefaultHandler
{
public:
static void serialize(QString, QList<LTMSettings>);
// unmarshall
bool startDocument();
bool endDocument();
bool endElement( const QString&, const QString&, const QString &qName );
bool startElement( const QString&, const QString&, const QString &name, const QXmlAttributes &attrs );
bool characters( const QString& str );
QList<LTMSettings> getSettings();
protected:
QString buffer;
LTMSettings setting;
MetricDetail metric;
int red, green, blue;
QList<LTMSettings> settings;
};
#endif

93
src/LTMOutliers.cpp Normal file
View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.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 <math.h>
#include <float.h>
#include <assert.h>
#include "LTMOutliers.h"
#include <QDebug>
LTMOutliers::LTMOutliers(double *xdata, double *ydata, int count, int windowsize, bool absolute) : stdDeviation(0.0)
{
double sum = 0;
int points = 0;
double allSum = 0.0;
int pos=0;
assert(count >= windowsize);
// initial samples from point 0 to windowsize
for (; pos < windowsize; ++pos) {
// we could either use a deviation of zero
// or base it on what we have so far...
// I chose to use sofar since spikes
// are common at the start of a ride
xdev add;
add.x = xdata[pos];
add.y = ydata[pos];
add.pos = pos;
if (absolute) add.deviation = fabs(ydata[pos] - (sum/windowsize));
else add.deviation = ydata[pos] - (sum/windowsize);
rank.append(add);
// when using -ve and +ve values stdDeviation is
// based upon the absolute value of deviation
// when not, we should only look at +ve values
if ((!absolute && add.deviation > 0) || absolute) {
allSum += add.deviation;
points++;
}
sum += ydata[pos]; // initialise the moving average
}
// bulk of samples from windowsize to the end
for (; pos<count; pos++) {
// ranked list
xdev add;
add.x = xdata[pos];
add.y = ydata[pos];
add.pos = pos;
if (absolute) add.deviation = fabs(ydata[pos] - (sum/windowsize));
else add.deviation = ydata[pos] - (sum/windowsize);
rank.append(add);
// calculate the sum for moving average
sum += ydata[pos] - ydata[pos-windowsize];
// when using -ve and +ve values stdDeviation is
// based upon the absolute value of deviation
// when not, we should only look at +ve values
if ((!absolute && add.deviation > 0) || absolute) {
allSum += add.deviation;
points++;
}
}
// and to the list of deviations
// calculate the average deviation across all points
stdDeviation = allSum / (double)points;
// create a ranked list
qSort(rank);
}

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