Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ba7b0cb3 | ||
|
|
3ad5401b0b | ||
|
|
d9e54c2be0 | ||
|
|
eeadc688bf | ||
|
|
ed9baf6fa3 | ||
|
|
3115bec92f | ||
|
|
2294b85e3b | ||
|
|
d1a1d56d6f | ||
|
|
cd576a4e27 | ||
|
|
03a221c558 | ||
|
|
9ed9f7a6d0 | ||
|
|
a78bd5d919 | ||
|
|
e4ece50af2 | ||
|
|
7611a44a54 | ||
|
|
8e43eb31aa | ||
|
|
a3f643e11c | ||
|
|
d348344e0e | ||
|
|
7b0fa7680d | ||
|
|
565ba3f219 | ||
|
|
db2b68bd9d | ||
|
|
d746dde5a3 | ||
|
|
ac43bccd6f | ||
|
|
ac2e202af4 | ||
|
|
b107c4a1e6 | ||
|
|
dcb441b7e5 | ||
|
|
dae942a2f7 | ||
|
|
97cedad044 | ||
|
|
8fec614a5e | ||
|
|
5463be1a2f | ||
|
|
e2e063015e | ||
|
|
7b7ff9622e | ||
|
|
cd6b86c3eb | ||
|
|
b01c007ed2 | ||
|
|
acdde3e02a | ||
|
|
6b07997791 | ||
|
|
440a0b8404 | ||
|
|
d3f16313bb | ||
|
|
fa0aa3fd75 | ||
|
|
0d811ba4ba | ||
|
|
9dff6e0cf6 | ||
|
|
b04e308d9f | ||
|
|
63fe2fb443 | ||
|
|
34001d30b5 | ||
|
|
769fd633e2 | ||
|
|
eb456cee63 | ||
|
|
afe4710bf5 | ||
|
|
b977abffcc | ||
|
|
8e1ec66820 | ||
|
|
adc8430e0e | ||
|
|
65109d95fd | ||
|
|
189f11d80f | ||
|
|
183564d1ea | ||
|
|
076190161f | ||
|
|
f43a71f3a4 | ||
|
|
c383ee75fb | ||
|
|
e0711b4bb7 | ||
|
|
5ce1617667 | ||
|
|
0091defb6c | ||
|
|
3d93c0ca52 | ||
|
|
8fcbe81daf | ||
|
|
524ab81a08 | ||
|
|
ad9a76a172 | ||
|
|
5b838d99c2 | ||
|
|
b7bfb98dc8 | ||
|
|
7352245a91 | ||
|
|
bfa68faeda | ||
|
|
95d6a40ecd | ||
|
|
3a4b782a16 | ||
|
|
7331775e40 | ||
|
|
0c7abe9755 | ||
|
|
9ab5fb26e2 | ||
|
|
a641ec7e0a | ||
|
|
c513a47e07 | ||
|
|
a562d2f73b | ||
|
|
dfbb1c29f8 | ||
|
|
b0cca3c2fa | ||
|
|
8c2eac427b | ||
|
|
880c97c639 | ||
|
|
614f267a5e | ||
|
|
14f7924c28 | ||
|
|
7a501a9699 | ||
|
|
f1238fcce6 | ||
|
|
4784408106 | ||
|
|
182208c145 | ||
|
|
00959bed8c | ||
|
|
57c7260a19 | ||
|
|
a70cf8ebc0 | ||
|
|
0f9b82a750 | ||
|
|
8cca3c088a | ||
|
|
d84ffec0a6 | ||
|
|
20477e1670 | ||
|
|
610b2ea2d0 | ||
|
|
e7a7803f09 | ||
|
|
c6a376b89f | ||
|
|
65c4d89890 | ||
|
|
1674558dbb | ||
|
|
ee79a86c1f | ||
|
|
92897a966b | ||
|
|
ca1c4def3a | ||
|
|
05346eda24 | ||
|
|
bea79092ab | ||
|
|
e478c24650 | ||
|
|
96bb21d04d | ||
|
|
65615295c2 | ||
|
|
b21d24039c | ||
|
|
c25f920062 | ||
|
|
7f2b6dd793 | ||
|
|
148390ea61 | ||
|
|
c525a36ea5 | ||
|
|
627595175f | ||
|
|
09365a8b24 | ||
|
|
8ae7a3f738 | ||
|
|
2ef0533ec3 | ||
|
|
b7632a4173 | ||
|
|
29fa978b8f | ||
|
|
1fbaeae611 | ||
|
|
d91337e18d | ||
|
|
88967c1588 | ||
|
|
dd7af03785 | ||
|
|
0c1353aa39 | ||
|
|
8cb592d6b4 | ||
|
|
09bec66b38 | ||
|
|
c4f379d12c | ||
|
|
1101e7e62b | ||
|
|
7aceb4f0f3 | ||
|
|
57b9e28110 | ||
|
|
0bf19e3e8d | ||
|
|
6815fe0d1f | ||
|
|
8913b37346 | ||
|
|
3ff839c4ff | ||
|
|
470885df50 | ||
|
|
152239eea4 | ||
|
|
073079a6e7 | ||
|
|
963c28c7a8 | ||
|
|
7e42cdd486 | ||
|
|
c1fc674609 | ||
|
|
615737658d | ||
|
|
3cab2f6175 | ||
|
|
93ad436c6a | ||
|
|
550ae22aa3 | ||
|
|
ea11916a93 | ||
|
|
ff3a052415 | ||
|
|
57a2c27262 | ||
|
|
c780f2edd0 | ||
|
|
4b0ce34d09 | ||
|
|
241976634d | ||
|
|
c21ca878c0 | ||
|
|
30ea14bcd5 | ||
|
|
e1f9bbf62e | ||
|
|
71b512c1a9 | ||
|
|
544718099a | ||
|
|
789be5681d | ||
|
|
66d32bbebf | ||
|
|
7f55855f2d | ||
|
|
532fe0d26c | ||
|
|
a29109343f | ||
|
|
5f13f4800b | ||
|
|
12fb154f5b | ||
|
|
72604b6cb3 | ||
|
|
dde17f278b | ||
|
|
7e25eedb8e | ||
|
|
d483445291 | ||
|
|
33076c1cb5 | ||
|
|
d6d9bd9227 | ||
|
|
59ae5fc537 | ||
|
|
0e5e5206a2 | ||
|
|
f6b5cd2790 | ||
|
|
87a6f9e628 | ||
|
|
9d9b447044 | ||
|
|
da8e636e65 | ||
|
|
e3c6e7e76c | ||
|
|
819303b060 | ||
|
|
fc583e6404 | ||
|
|
830e4efd3d | ||
|
|
3e0f3358f5 | ||
|
|
f690e188a1 | ||
|
|
cffde51caa | ||
|
|
5e2f52cf28 | ||
|
|
603c56f595 | ||
|
|
cee09061ff | ||
|
|
91a66a7520 | ||
|
|
d106086afe | ||
|
|
0238ba9052 | ||
|
|
acc522748a | ||
|
|
99c330edc2 | ||
|
|
ea05ba2151 | ||
|
|
0d6949ed9e | ||
|
|
2f41cabb1d | ||
|
|
07e086ce44 | ||
|
|
f31cef3f1e | ||
|
|
a4828070df | ||
|
|
781e0619ec | ||
|
|
a8e5777953 | ||
|
|
dd5cdd920d | ||
|
|
0139b7ee84 | ||
|
|
78111d4279 | ||
|
|
899e6eb362 | ||
|
|
f6eb97ec0f | ||
|
|
48a25081ed | ||
|
|
5e575beaff | ||
|
|
861fa5ee38 | ||
|
|
fef237f138 | ||
|
|
21572977b4 | ||
|
|
9109eb616f | ||
|
|
de0b28f9dc | ||
|
|
02fe34216d | ||
|
|
081856bf64 | ||
|
|
62d658c929 | ||
|
|
29069cd63f | ||
|
|
0fe01407a2 | ||
|
|
efd4de62a1 | ||
|
|
2962fce0b7 | ||
|
|
8e73d01829 | ||
|
|
ca24400a0a | ||
|
|
4465d18b4f | ||
|
|
3e2468dcc6 | ||
|
|
e1d69f71e1 | ||
|
|
1a71a8a41f | ||
|
|
1506c41cca | ||
|
|
cd3bbc4e64 | ||
|
|
fc668303da | ||
|
|
8569f812a6 | ||
|
|
0b7140161c | ||
|
|
06e44b8d47 | ||
|
|
51784f64f6 | ||
|
|
c4f82e19b6 | ||
|
|
7941f0a9bc | ||
|
|
d03d538498 | ||
|
|
8741af20ff | ||
|
|
a0c5669166 | ||
|
|
3f4adc2d31 | ||
|
|
fc7dce1634 | ||
|
|
803383ba2c | ||
|
|
9ae79db4cb | ||
|
|
74f85f08d5 | ||
|
|
85ee3859e4 | ||
|
|
a6917a682a | ||
|
|
d77c6ed576 | ||
|
|
ac570c415c | ||
|
|
4ca86c441d | ||
|
|
fbdd15f218 | ||
|
|
afb9a44fe2 | ||
|
|
1751d8bf12 | ||
|
|
fdb7d124d9 | ||
|
|
07623bf94f | ||
|
|
906b6f0a18 | ||
|
|
744294dd53 | ||
|
|
38df7c28bd | ||
|
|
778d651f00 | ||
|
|
f820627fda | ||
|
|
b1f71eda4c | ||
|
|
d64fc6ea85 | ||
|
|
4e7e6cfb3a | ||
|
|
02a60735f0 | ||
|
|
5c2a2f0527 | ||
|
|
281c357605 | ||
|
|
d1f003c190 | ||
|
|
cd4fe5fe2e | ||
|
|
92725db36a | ||
|
|
d2831baacc | ||
|
|
fbf4f988c9 | ||
|
|
636e8f3895 | ||
|
|
f8ab4b03dc | ||
|
|
4552cafdb6 | ||
|
|
883dbb448f | ||
|
|
a217243456 | ||
|
|
affed979ba | ||
|
|
32d67f21eb | ||
|
|
c0437f30e3 | ||
|
|
26b00e184c | ||
|
|
7f2abaa01b | ||
|
|
ccc2f1f0ff | ||
|
|
e0f6cf23e6 | ||
|
|
f980142110 | ||
|
|
0785cf863e | ||
|
|
19fe016567 | ||
|
|
ff3a232863 | ||
|
|
bd28d3b28e | ||
|
|
c6d28370ec | ||
|
|
f523fd5d3c | ||
|
|
e47847c19e | ||
|
|
6919d186bf | ||
|
|
0fd735e16d | ||
|
|
a02bfaf810 | ||
|
|
afdc862cc2 | ||
|
|
19223e51b3 | ||
|
|
37a17f11f8 | ||
|
|
19aff56946 | ||
|
|
378b6f3537 | ||
|
|
268afe536c | ||
|
|
04b698f255 | ||
|
|
f97bc9c152 | ||
|
|
a392b33b51 | ||
|
|
81a43b5314 | ||
|
|
c2f3476569 | ||
|
|
d4edc12dc6 | ||
|
|
b68e55beca | ||
|
|
bc85a3b5fe | ||
|
|
fda33927d4 | ||
|
|
7bb9cf5462 | ||
|
|
cef5cca454 | ||
|
|
77eccc7797 | ||
|
|
e3ac6c799c | ||
|
|
906900fb19 | ||
|
|
4145282415 | ||
|
|
3eb5243a28 | ||
|
|
885629a2f5 | ||
|
|
07a393fb64 | ||
|
|
da28b43d40 | ||
|
|
4f9850bd9d | ||
|
|
f930c6f272 | ||
|
|
fae1ea3f92 | ||
|
|
4b615a6cda | ||
|
|
b491867386 | ||
|
|
f323780848 | ||
|
|
9147369c41 | ||
|
|
5cd621f800 | ||
|
|
1f548d0b84 | ||
|
|
16bc8c2686 | ||
|
|
fbd5238e4e | ||
|
|
67919e4d21 |
BIN
doc/3d.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
24
doc/Makefile
@@ -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
|
After Width: | Height: | Size: 48 KiB |
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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™</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
|
After Width: | Height: | Size: 155 KiB |
@@ -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
|
After Width: | Height: | Size: 311 KiB |
@@ -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,
|
||||
|
||||
BIN
doc/logo.jpg
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
doc/logo.png
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 159 KiB |
BIN
doc/map.png
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
doc/metrics-power.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
doc/metrics-timedist.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
doc/metrics-tiz.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
399
doc/older-releases.content
Normal 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™</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
|
After Width: | Height: | Size: 92 KiB |
BIN
doc/realtime.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
81
doc/release-notes.content
Normal 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>
|
||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 192 KiB |
BIN
doc/ride-plot2.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 134 KiB |
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
355
src/AllPlot.cpp
@@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &);
|
||||
};
|
||||
|
||||
@@ -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); }
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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™"); }
|
||||
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™");
|
||||
}
|
||||
void initialize() {
|
||||
#endif
|
||||
setName(tr("BikeScore™"));
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
401
src/DBAccess.cpp
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ class DeviceConfiguration
|
||||
|
||||
bool isDefaultDownload, // not implemented yet
|
||||
isDefaultRealtime; // not implemented yet
|
||||
|
||||
int postProcess;
|
||||
};
|
||||
|
||||
class DeviceConfigurations
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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&v=2&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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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("™"); // 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( "™", 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("™"); // process html encoding of(TM)
|
||||
QString tm = trademark.toPlainText();
|
||||
|
||||
QString s = string;
|
||||
s.replace( tm, "™" );
|
||||
s.replace( "&", "&" );
|
||||
s.replace( ">", ">" );
|
||||
s.replace( "<", "<" );
|
||||
s.replace( "\"", """ );
|
||||
s.replace( "\'", "'" );
|
||||
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
@@ -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
@@ -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);
|
||||
}
|
||||