Compare commits
450 Commits
master
...
release_1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b87aab0302 | ||
|
|
ecdc2288ff | ||
|
|
04bb484f8e | ||
|
|
6eeea4a305 | ||
|
|
af676f24eb | ||
|
|
e761091097 | ||
|
|
72c40de966 | ||
|
|
243a28bb87 | ||
|
|
5299810a7e | ||
|
|
b86921d90c | ||
|
|
570b2ffc73 | ||
|
|
65ee84f948 | ||
|
|
0873317ea7 | ||
|
|
236878ff7f | ||
|
|
f71d329f14 | ||
|
|
eb0a40ba8e | ||
|
|
e7c7a43b8d | ||
|
|
29ad88de64 | ||
|
|
879b1f6a2e | ||
|
|
50428b5586 | ||
|
|
dc8877eb6a | ||
|
|
fbc3c939e2 | ||
|
|
d65fd2a4d0 | ||
|
|
656f548896 | ||
|
|
c798420cee | ||
|
|
d785ca5a0f | ||
|
|
3567012046 | ||
|
|
e7f7decf10 | ||
|
|
df33fe2301 | ||
|
|
325140af26 | ||
|
|
afb3fb6d62 | ||
|
|
7bd0d0326c | ||
|
|
a41226ade5 | ||
|
|
bab4063fd0 | ||
|
|
f4336ec87c | ||
|
|
9fb219019e | ||
|
|
32e30f7ccd | ||
|
|
75e2c43cdd | ||
|
|
0c08ff093d | ||
|
|
faf01deb6f | ||
|
|
dc50cf79e6 | ||
|
|
4399d1ad81 | ||
|
|
86cfac2d9e | ||
|
|
a1bbf4d50f | ||
|
|
debd870811 | ||
|
|
ad0fdd243e | ||
|
|
d03defb2da | ||
|
|
a5dd700033 | ||
|
|
ec8d3e9949 | ||
|
|
55f0b19ff5 | ||
|
|
75a5d66a6a | ||
|
|
0200f3189c | ||
|
|
acaa6e1f1a | ||
|
|
b37c80f849 | ||
|
|
1d85f94f7b | ||
|
|
ce2ae9a8d3 | ||
|
|
a7023c2ea5 | ||
|
|
a970b12f68 | ||
|
|
d66ca54b41 | ||
|
|
e9e3262caa | ||
|
|
d2efc75948 | ||
|
|
e281cfb444 | ||
|
|
3722c3bdf0 | ||
|
|
59805db47c | ||
|
|
143355459d | ||
|
|
5c0bdd8969 | ||
|
|
2be608410a | ||
|
|
bf41ffdb10 | ||
|
|
0c82211bf6 | ||
|
|
20b7c401ad | ||
|
|
59d0b0ede1 | ||
|
|
086eb6c26b | ||
|
|
d3ea473f60 | ||
|
|
5a453fb210 | ||
|
|
3f7938f344 | ||
|
|
e5ec325caf | ||
|
|
278fd14af7 | ||
|
|
453a398663 | ||
|
|
1adbef36e8 | ||
|
|
1409d9ec34 | ||
|
|
e31db9f837 | ||
|
|
d6022ec28c | ||
|
|
31c3b508bb | ||
|
|
9ecf06a334 | ||
|
|
83fb5c6968 | ||
|
|
b5f1e64cae | ||
|
|
e3e1f8fe82 | ||
|
|
e8a7a4bf4d | ||
|
|
622516b63d | ||
|
|
41063d069d | ||
|
|
a237779dc8 | ||
|
|
fc4108904f | ||
|
|
6ce20ccb29 | ||
|
|
83ce4d1f59 | ||
|
|
5461e07fcd | ||
|
|
502cb4b60f | ||
|
|
9624cd03a9 | ||
|
|
a01bc21ece | ||
|
|
f588082449 | ||
|
|
356ee341b2 | ||
|
|
563285ab9d | ||
|
|
6dee00e5b8 | ||
|
|
6e56b8652f | ||
|
|
b4553ed189 | ||
|
|
1c732ed2cc | ||
|
|
f765cef661 | ||
|
|
e9bae94b83 | ||
|
|
913b5b6c73 | ||
|
|
eb06e3e6d7 | ||
|
|
ca4278f904 | ||
|
|
df9519a027 | ||
|
|
d79a6f5885 | ||
|
|
71fcf7ba5d | ||
|
|
ad8e5015e1 | ||
|
|
d500064da2 | ||
|
|
9f65afff15 | ||
|
|
07b8d15769 | ||
|
|
f8d6190553 | ||
|
|
ee7a953e53 | ||
|
|
0754c43b1f | ||
|
|
64751d7585 | ||
|
|
263e28b6b6 | ||
|
|
8dc03b0cb3 | ||
|
|
7f1a19930c | ||
|
|
8af2bb02e5 | ||
|
|
0b9458d954 | ||
|
|
2ddd87faae | ||
|
|
13ec408aa3 | ||
|
|
47b4cf1c44 | ||
|
|
e973e51281 | ||
|
|
38f8283ece | ||
|
|
e8f2877539 | ||
|
|
52821ee647 | ||
|
|
fcdd894c52 | ||
|
|
d9baad3545 | ||
|
|
8d3fbf85e1 | ||
|
|
af633953ec | ||
|
|
76f6cf3f7f | ||
|
|
3ab2c91ce2 | ||
|
|
13deea6f31 | ||
|
|
3723185439 | ||
|
|
69775fccb9 | ||
|
|
08fc58454f | ||
|
|
6caf431d05 | ||
|
|
f11d7e1bd9 | ||
|
|
fb4514b5fe | ||
|
|
e8cddb104f | ||
|
|
1652491be1 | ||
|
|
12f91a9bf1 | ||
|
|
3b4e902870 | ||
|
|
d2545348a1 | ||
|
|
b90598eb39 | ||
|
|
6ccaac6123 | ||
|
|
15a546927e | ||
|
|
bf534977b8 | ||
|
|
d92468a625 | ||
|
|
47044708d9 | ||
|
|
9fefe27a38 | ||
|
|
483589c372 | ||
|
|
420b2b6b44 | ||
|
|
84f4250e4a | ||
|
|
2e1801d8bd | ||
|
|
4453d690b3 | ||
|
|
b2acd45c8d | ||
|
|
ea58d961a6 | ||
|
|
40120a373e | ||
|
|
ca26791dcb | ||
|
|
ea49b24337 | ||
|
|
02dce7245b | ||
|
|
12db84372f | ||
|
|
78766b0752 | ||
|
|
5c2f829519 | ||
|
|
015862460e | ||
|
|
4633562138 | ||
|
|
aeec208f10 | ||
|
|
37fdc5eefd | ||
|
|
095ad55763 | ||
|
|
9dc3c00023 | ||
|
|
7e6ee45d9a | ||
|
|
8506306627 | ||
|
|
604c24147f | ||
|
|
4604f62e23 | ||
|
|
0f0e817ff0 | ||
|
|
bc17462c41 | ||
|
|
6222fe888a | ||
|
|
c9a641ea61 | ||
|
|
93b5007746 | ||
|
|
53f4e6f224 | ||
|
|
48bd6b2fec | ||
|
|
4861d91c95 | ||
|
|
4ac1444f5c | ||
|
|
3fe277a9a5 | ||
|
|
0d1066a020 | ||
|
|
22be6e216d | ||
|
|
7209df0764 | ||
|
|
3f1d3f2adb | ||
|
|
73814a4df8 | ||
|
|
9228211e15 | ||
|
|
0f218f7cdd | ||
|
|
4bc62f7a6e | ||
|
|
4978209289 | ||
|
|
57e5fea35b | ||
|
|
b7c40388f2 | ||
|
|
c7652caeec | ||
|
|
21d5576393 | ||
|
|
3db61084a1 | ||
|
|
56cc8b084d | ||
|
|
6cfccfc92f | ||
|
|
f328248582 | ||
|
|
9d22cc3dc8 | ||
|
|
a65460d5d8 | ||
|
|
ee3b9f46b9 | ||
|
|
d769625f5c | ||
|
|
071c9b1071 | ||
|
|
b6a902ebd9 | ||
|
|
9e73576fba | ||
|
|
b046ae538b | ||
|
|
52b2049949 | ||
|
|
92749ac705 | ||
|
|
9e4d237ce9 | ||
|
|
a6f269363e | ||
|
|
03e2f95c43 | ||
|
|
f7ea9b236e | ||
|
|
7026520ec4 | ||
|
|
842303029c | ||
|
|
1292a5f8e9 | ||
|
|
a8dad052fd | ||
|
|
50a9de052c | ||
|
|
5a00528f4d | ||
|
|
b4584baf03 | ||
|
|
d849834070 | ||
|
|
21a72bc45e | ||
|
|
264e8b118e | ||
|
|
fe5b1300eb | ||
|
|
c731525124 | ||
|
|
dd7c308667 | ||
|
|
700ac5c12d | ||
|
|
f685703ae4 | ||
|
|
8d2edd4c48 | ||
|
|
f8a94dc767 | ||
|
|
f1ade25fa7 | ||
|
|
33ee8daf1e | ||
|
|
fb1b79cccf | ||
|
|
c11b305239 | ||
|
|
baaacda681 | ||
|
|
1105d60d1f | ||
|
|
d5997b9fee | ||
|
|
f2187c6965 | ||
|
|
801a26392e | ||
|
|
ec38e8ca1d | ||
|
|
61161a7b5d | ||
|
|
5a3c3c8eb7 | ||
|
|
a9ce6ae947 | ||
|
|
ef5f2c1a47 | ||
|
|
6c3ff75f0b | ||
|
|
1e9c4dffe8 | ||
|
|
80e113d347 | ||
|
|
ff59009f86 | ||
|
|
9d557b26a1 | ||
|
|
91f114f199 | ||
|
|
7c72da0c72 | ||
|
|
babbaa7e2c | ||
|
|
f91e45d950 | ||
|
|
6aa6693cad | ||
|
|
41da1dfc68 | ||
|
|
d91608ecca | ||
|
|
25b17de0e4 | ||
|
|
e6c85a12f4 | ||
|
|
4eeb656016 | ||
|
|
b6f817c4d7 | ||
|
|
83d6989d51 | ||
|
|
e63452d521 | ||
|
|
812b17c952 | ||
|
|
e2d9a96b52 | ||
|
|
133f677b12 | ||
|
|
da214db96d | ||
|
|
98ede8e40c | ||
|
|
65c4e1d277 | ||
|
|
6e5487ca39 | ||
|
|
b519a0384b | ||
|
|
3c998c98b0 | ||
|
|
2f3df889c5 | ||
|
|
60f9724543 | ||
|
|
7cf5766ab7 | ||
|
|
91d51a6246 | ||
|
|
c832727a03 | ||
|
|
73fdb7d6ef | ||
|
|
bfadc8c043 | ||
|
|
cd080aa48c | ||
|
|
f27ff27c3b | ||
|
|
aed29e150d | ||
|
|
4434239235 | ||
|
|
8db3e2c0b2 | ||
|
|
f34e0fc74c | ||
|
|
51165d0acf | ||
|
|
2e21b6e328 | ||
|
|
b47ac76116 | ||
|
|
17cbe38af8 | ||
|
|
bce0fbdb95 | ||
|
|
9f56747a6d | ||
|
|
262fa0d9b1 | ||
|
|
76f161fb46 | ||
|
|
9e7c9407ff | ||
|
|
371bfbdae3 | ||
|
|
c70685ee4a | ||
|
|
55356eb221 | ||
|
|
b6691939f1 | ||
|
|
8ea444f55e | ||
|
|
7fd58c7f0d | ||
|
|
6e60d167b7 | ||
|
|
d5fd8c1234 | ||
|
|
8b782dff27 | ||
|
|
7a43765a25 | ||
|
|
eae6f74087 | ||
|
|
2c2d1eedb6 | ||
|
|
26e0336f62 | ||
|
|
ad32b3ed94 | ||
|
|
304fbb49b4 | ||
|
|
d16330134d | ||
|
|
aba9a29a43 | ||
|
|
5dc93dccc8 | ||
|
|
156a053666 | ||
|
|
e68caf412b | ||
|
|
7db9fd58f4 | ||
|
|
676838bc70 | ||
|
|
76be545eee | ||
|
|
74fef7e468 | ||
|
|
cc8a22e2dd | ||
|
|
670e172e26 | ||
|
|
f2b2350312 | ||
|
|
048ce08ca9 | ||
|
|
f66e94c05a | ||
|
|
d2532341b4 | ||
|
|
83e9f67755 | ||
|
|
fbff0d6df1 | ||
|
|
32dc259642 | ||
|
|
bb3afec9eb | ||
|
|
f691758e7e | ||
|
|
b03edad5f5 | ||
|
|
c852547fb3 | ||
|
|
b97eaa4bf5 | ||
|
|
3c1f6f5e27 | ||
|
|
8b02b6620d | ||
|
|
b723d43bc1 | ||
|
|
9e09e4fe70 | ||
|
|
42eb95c8b3 | ||
|
|
134f22f0ca | ||
|
|
4a8ff92437 | ||
|
|
b19783c469 | ||
|
|
8373bbaf09 | ||
|
|
b8827105b8 | ||
|
|
bd73c83b25 | ||
|
|
b658247509 | ||
|
|
f402d9afd8 | ||
|
|
abbe205dc9 | ||
|
|
253e2083f7 | ||
|
|
a9faed9361 | ||
|
|
629d9c7db2 | ||
|
|
139c7c03fc | ||
|
|
ebdbff5f0e | ||
|
|
fd0e5f76be | ||
|
|
8ebc1a4eb2 | ||
|
|
e2fc9bce5c | ||
|
|
d01184905f | ||
|
|
866eec4eee | ||
|
|
27bf8ae1ef | ||
|
|
abdf74ca5a | ||
|
|
0cd28ba8d0 | ||
|
|
fff8f9a0e6 | ||
|
|
edd39b3d3b | ||
|
|
3cdc85e411 | ||
|
|
1f8ce83475 | ||
|
|
ae1c59d738 | ||
|
|
d87c0500ab | ||
|
|
9cdda62cfa | ||
|
|
753977743e | ||
|
|
43cf7e0126 | ||
|
|
e103824538 | ||
|
|
cfdbe3cc30 | ||
|
|
9dd02b69e4 | ||
|
|
29c760cdeb | ||
|
|
de66eed5ca | ||
|
|
c00bc0504d | ||
|
|
db43014375 | ||
|
|
b5b16a4768 | ||
|
|
e1371fab8c | ||
|
|
b967d276d1 | ||
|
|
6952d2b82c | ||
|
|
859b1f2842 | ||
|
|
acd48e7112 | ||
|
|
ff3227f520 | ||
|
|
432820a24e | ||
|
|
eefe0373c8 | ||
|
|
56970da25c | ||
|
|
f2fe16ed51 | ||
|
|
efc99a8a26 | ||
|
|
2f7d42c46f | ||
|
|
69612c8d59 | ||
|
|
7e0731c7f8 | ||
|
|
abf2f5b349 | ||
|
|
7b52bc6f73 | ||
|
|
aefc1d18e8 | ||
|
|
d7daa3d54d | ||
|
|
3a2a469d58 | ||
|
|
f6968f98bf | ||
|
|
2729800d42 | ||
|
|
8c7dfdeecd | ||
|
|
8a3bc4fd44 | ||
|
|
3e7d53b418 | ||
|
|
0dc0f48416 | ||
|
|
6bee5b3ca0 | ||
|
|
dd9ff12914 | ||
|
|
f18ea0d646 | ||
|
|
54ab7d02c3 | ||
|
|
6fc7669b71 | ||
|
|
4ac73d88ac | ||
|
|
a341bc4302 | ||
|
|
1143a30248 | ||
|
|
04aadf4ee3 | ||
|
|
f726979a01 | ||
|
|
dc1fcfef35 | ||
|
|
6a121a7fc7 | ||
|
|
c24e341b3d | ||
|
|
9f6a0ad7d6 | ||
|
|
e9628702ed | ||
|
|
a179ec592a | ||
|
|
0e09790c5b | ||
|
|
5593c8e3f7 | ||
|
|
a2f233b345 | ||
|
|
a5b621a13d | ||
|
|
2b23610f5f | ||
|
|
491ef5c639 | ||
|
|
4283c5b408 | ||
|
|
fa45b5c980 | ||
|
|
81c5cf80b3 | ||
|
|
982fb843cf | ||
|
|
3b23740f62 | ||
|
|
78fbe4e643 | ||
|
|
d2abe9716f | ||
|
|
6134c947ed | ||
|
|
035226c3b5 | ||
|
|
734d025b7f | ||
|
|
e4e6c39aba | ||
|
|
8bd578057f | ||
|
|
c850170cfc | ||
|
|
7f2d13a779 | ||
|
|
f628170850 | ||
|
|
79b83fd668 | ||
|
|
861507bb30 | ||
|
|
1de131f122 |
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# old skool
|
||||
.svn
|
||||
|
||||
# osx noise
|
||||
.DS_Store
|
||||
profile
|
||||
31
Makefile
@@ -1,31 +0,0 @@
|
||||
#
|
||||
# $Id: Makefile,v 1.11 2006/09/06 23:23:03 srhea Exp $
|
||||
#
|
||||
# Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation; either version 2 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#
|
||||
|
||||
SUBDIRS=doc src
|
||||
|
||||
all: subdirs
|
||||
.PHONY: all subdirs clean
|
||||
|
||||
clean:
|
||||
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir clean; done
|
||||
|
||||
subdirs:
|
||||
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir; done
|
||||
|
||||
9
doc/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# old skool
|
||||
.svn
|
||||
|
||||
# osx noise
|
||||
.DS_Store
|
||||
profile
|
||||
|
||||
#html files are auto-generated by the scripts:
|
||||
*.html
|
||||
22
doc/Makefile
@@ -4,11 +4,10 @@ HTML=$(subst .content,.html,$(CONTENT))
|
||||
TARBALLS=$(wildcard gc_*.tgz)
|
||||
OTHER=logo.jpg sample.gp sample.png cpint.gp cpint.png \
|
||||
screenshot-summary.png screenshot-plot.png \
|
||||
screenshot-cpint.png screenshot-phist.png
|
||||
DISTRIB=GoldenCheetah_2006-12-25_Darwin_powerpc.dmg \
|
||||
GoldenCheetah_2006-09-06_Darwin_powerpc.dmg \
|
||||
GoldenCheetah_2006-09-07_Darwin_powerpc.dmg \
|
||||
GoldenCheetah_2006-09-19_Darwin_powerpc.dmg
|
||||
screenshot-cpint.png screenshot-phist.png \
|
||||
screenshot-download.png screenshot-weekly.png \
|
||||
choose-a-cyclist.png main-window.png critical-power.png \
|
||||
power.zones
|
||||
|
||||
all: $(HTML)
|
||||
.PHONY: all clean install
|
||||
@@ -17,8 +16,11 @@ clean:
|
||||
rm -f $(HTML)
|
||||
|
||||
install:
|
||||
rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) $(DISTRIB) \
|
||||
srhea.net:public_html/goldencheetah/
|
||||
rsync -avz -e ssh $(HTML) $(TARBALLS) $(OTHER) \
|
||||
srhea.net:wwwroot/goldencheetah.org/
|
||||
|
||||
command-line.html: command-line.content genpage.pl
|
||||
./genpage.pl "Legacy Command-Line Tools" $< > $@
|
||||
|
||||
contact.html: contact.content genpage.pl
|
||||
./genpage.pl "Contact Us" $< > $@
|
||||
@@ -47,3 +49,9 @@ search.html: search.content genpage.pl
|
||||
users-guide.html: users-guide.content genpage.pl
|
||||
./genpage.pl "User's Guide" $< > $@
|
||||
|
||||
wishlist.html: wishlist.content genpage.pl
|
||||
./genpage.pl "Wish List" $< > $@
|
||||
|
||||
zones.html: zones.content genpage.pl
|
||||
./genpage.pl "Power Zones File Guide" $< > $@
|
||||
|
||||
|
||||
BIN
doc/cheetah_logo.eps
Normal file
BIN
doc/choose-a-cyclist.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
232
doc/command-line.content
Normal file
@@ -0,0 +1,232 @@
|
||||
<!-- $Id: users-guide.content,v 1.5 2006/05/27 16:32:46 srhea Exp $ -->
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Using the Command Line Utilities
|
||||
</font></big>
|
||||
|
||||
<p>
|
||||
In addition to the GUI, Golden Cheetah comes with
|
||||
several command line utilities:
|
||||
<code>ptdl</code>, which downloads ride data from a PowerTap Pro version 2.21
|
||||
cycling computer, <code>ptunpk</code>, which unpacks the raw bytes downloaded
|
||||
by <code>ptdl</code> and outputs more human-friendly ride information, and
|
||||
<code>cpint</code>, which computes your critical power (see below). We've
|
||||
also written several Perl scripts to help you graph and summarize the data.
|
||||
|
||||
<p>
|
||||
NOTE: We no longer support the use of the command-line tools. Please use the
|
||||
graphical version of GoldenCheetah instead. This documentation is here for
|
||||
the benefit of the brave alone.
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Extracting the Data
|
||||
</font></big>
|
||||
<p>
|
||||
First, make sure you have the FTDI drivers installed, as described in the <a
|
||||
href="users-guide.html">User's Guide</a>. You can then run <code>ptdl</code>
|
||||
without arguments:
|
||||
|
||||
<pre>
|
||||
$ ./ptdl
|
||||
Reading from /dev/tty.usbserial-3B1.
|
||||
Reading version information...done.
|
||||
Reading ride time...done.
|
||||
Writing to 2006_05_15_11_34_03.raw.
|
||||
Reading ride data..............done.
|
||||
$ head -5 2006_05_15_11_34_03.raw
|
||||
57 56 55 64 02 15
|
||||
60 06 05 0f 6b 22
|
||||
40 08 30 00 00 00
|
||||
86 0e 74 99 00 55
|
||||
81 06 77 a8 40 55
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
If everything goes well, <code>ptdl</code> will automatically detect the
|
||||
device (<code>/dev/tty.usbserial-3B1</code> in the example above), read the
|
||||
ride data from it, and write to a file named by the date and time at which the
|
||||
ride started (<code>2006_05_15_11_34_03.raw</code> in the example; the format
|
||||
is YYYY_MM_DD_hh_mm_ss.raw).
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Unpacking the Data
|
||||
</font></big>
|
||||
<p>As shown by the <code>head</code> command above, the data in this
|
||||
<code>.raw</code> file is just the raw bytes that represent your ride. To
|
||||
unpack those bytes and display them in a more human-friendly format, use
|
||||
<code>ptunpk</code>:
|
||||
|
||||
<pre>
|
||||
$ ./ptunpk 2006_05_15_11_34_03.raw
|
||||
$ head -5 2006_05_15_11_34_03.dat
|
||||
# Time Torq MPH Watts Miles Cad HR Int
|
||||
# 2006/5/15 11:34:03 1147707243
|
||||
# wheel size=2096 mm, interval=0, rec int=1
|
||||
0.021 13.1 2.450 43 0.00781 0 85 0
|
||||
0.042 13.4 5.374 97 0.00912 64 85 0
|
||||
</pre>
|
||||
|
||||
<code>ptunpk</code> takes a <code>.raw</code> file for input and writes a
|
||||
<code>.dat</code> file as output. Lines that start with an ampersand ("#") in
|
||||
this file are comments; the other lines represent measured samples. As shown
|
||||
by the first comment in the file, the columns are: time in minutes, torque in
|
||||
Newton-meters, speed in miles per hour, power in watts, distance in miles,
|
||||
cadence, heart rate, and interval number.
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Summarizing the Data
|
||||
</font></big>
|
||||
<p>
|
||||
We hope to have a graphical interface to these programs soon, but until then,
|
||||
the only summarization tools we have are command-line programs. The script
|
||||
<code>intervals.pl</code> summarizes the intervals performed in a workout:
|
||||
|
||||
<small>
|
||||
<pre>
|
||||
$ ./intervals.pl 2006_05_03_16_24_04.dat
|
||||
Power Heart Rate Cadence Speed
|
||||
Int Dur Dist Avg Max Avg Max Avg Max Avg Max
|
||||
0 77:10 19.3 213 693 134 167 82 141 16.0 27.8
|
||||
1 4:03 0.9 433 728 175 203 84 122 13.0 18.8
|
||||
2 7:23 1.0 86 502 135 179 71 141 16.0 28.2
|
||||
3 4:27 0.9 390 628 170 181 70 100 12.0 17.6
|
||||
4 8:04 0.9 60 203 130 178 50 120 18.0 30.1
|
||||
5 4:30 0.9 384 682 170 179 79 113 11.0 18.6
|
||||
6 8:51 1.1 53 245 125 176 70 141 8.0 26.6
|
||||
7 2:48 0.4 400 614 164 178 62 91 8.0 13.6
|
||||
8 7:01 1.1 46 268 128 170 71 141 12.0 28.8
|
||||
9 4:30 0.9 379 560 168 180 81 170 11.0 18.3
|
||||
10 28:46 6.5 120 409 128 179 79 141 15.0 31.0
|
||||
</pre>
|
||||
</small>
|
||||
|
||||
<p>
|
||||
In the example above, a rider performed five hill intervals, four of which
|
||||
climbed a medium size hill that took about 4-5 minutes to climb (intervals
|
||||
1, 3, 5, and 9), and one on a shorter hill that took just under 3 minutes to
|
||||
climb (interval 7).
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Graphing the Data
|
||||
</font></big>
|
||||
<p>
|
||||
For graphing the data in the ride, we use <code>smooth.pl</code> and the
|
||||
<code>gnuplot</code> program. You can use <a href="sample.gp">sample.gp</a>
|
||||
to graph the power, heart rate, cadence, and speed for the hill workout above:
|
||||
|
||||
<pre>
|
||||
$ gnuplot sample.gp
|
||||
</pre>
|
||||
|
||||
<img align="center" alt="Sample Plot" src="sample.png">
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Finding Your "Critical Power"
|
||||
</font></big>
|
||||
<p>
|
||||
Joe Friel calls the maximum average power a rider can sustain over an interval
|
||||
the rider's "critical power" for that duration. The <code>cpint</code>
|
||||
program automatically computes your critical power over all interval lengths
|
||||
using the data from all your past rides. This program looks at all the
|
||||
<code>.raw</code> files in a directory, calculating your maximum power over
|
||||
every subinterval length and storing them in a corresponding <code>.cpi</code>
|
||||
file. It then combines the data in all of the <code>.cpi</code> files to find
|
||||
your critical power over <i>all</i> subintervals of <i>all</i> your rides.
|
||||
|
||||
<pre>
|
||||
$ ls *.raw
|
||||
2006_04_28_10_48_33.raw 2006_05_10_17_08_30.raw 2006_05_18_16_32_53.raw
|
||||
2006_05_03_16_24_04.raw 2006_05_13_10_29_12.raw 2006_05_21_12_25_07.raw
|
||||
2006_05_05_10_52_05.raw 2006_05_15_11_34_03.raw 2006_05_22_18_28_47.raw
|
||||
...
|
||||
2006_05_09_09_54_29.raw 2006_05_17_16_44_35.raw
|
||||
$ ./cpint
|
||||
Compiling data for ride on Fri Apr 28 10:48:33 2006...done.
|
||||
Compiling data for ride on Sat Apr 29 10:07:48 2006...done.
|
||||
Compiling data for ride on Sun Apr 30 14:00:17 2006...done.
|
||||
...
|
||||
Compiling data for ride on Mon May 22 18:28:47 2006...done.
|
||||
0.021 1264
|
||||
0.042 1221
|
||||
0.063 1216
|
||||
...
|
||||
5.019 391
|
||||
...
|
||||
171.885 163
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Over this set of rides, the rider's maximum power is 1264 watts, achieved over
|
||||
an interval of 0.021 minutes (1.26 seconds). Over all five-minute
|
||||
subintervals, he has achieved a maximum average power of 391 watts. The
|
||||
longest ride in this set was 171.885 minutes long, and he averaged 163 watts
|
||||
over it.
|
||||
|
||||
<p>
|
||||
We can graph the output of <code>cpint</code> using <code>gnuplot</code> with
|
||||
<a href="cpint.gp">cpint.gp</a>:
|
||||
|
||||
<pre>
|
||||
$ ./cpint > cpint.out
|
||||
$ gnuplot cpint.gp
|
||||
</pre>
|
||||
|
||||
<img src="cpint.png">
|
||||
|
||||
<p>
|
||||
The first time you run <code>cpint</code> it will take a while, as it has to
|
||||
analyze all your past rides. On subsequent runs, however, it will only
|
||||
analyze new files.
|
||||
|
||||
<p><i>Training and Racing with a Power Meter</i> (see the <a
|
||||
href="faq.html">FAQ</a>) contains a table of critical powers of Cat 5 cyclists
|
||||
up through international pros at interval lengths of 5 seconds, 1 minute, 5
|
||||
minutes, and 60 minutes. Using this table and the <code>cpint</code> program,
|
||||
you can determine whether you're stronger than others in your racing category
|
||||
at each interval length and adapt your training program accordingly.
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Converting Old Data
|
||||
</font></big>
|
||||
|
||||
<p>
|
||||
If you've used the PowerTuned software that comes with the PowerTap you may
|
||||
have lots of old ride data in that program that you'd like to include in your
|
||||
critical power graph. You can convert the <code>.xml</code> files that
|
||||
PowerTuned produces to <code>.raw</code> files using the <code>ptpk</code>
|
||||
program:
|
||||
|
||||
<p>
|
||||
<pre>
|
||||
$ ./ptpk 2006_04_27_00_23_28.xml
|
||||
$ head -5 2006_04_27_00_23_28.raw
|
||||
57 56 55 64 02 15
|
||||
60 06 04 7b 80 17
|
||||
40 08 30 00 00 00
|
||||
84 04 00 24 00 ff
|
||||
83 03 00 d7 00 ff
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
<code>ptpk</code> assumes the input <code>.xml</code> file was generated with
|
||||
a wheel size of 2,096 mm and a recording interval of 1. If this is not the
|
||||
case, you should specify the correct values with the <code>-w</code> and
|
||||
<code>-r</code> options.
|
||||
|
||||
<p>
|
||||
Note that the PowerTuned software computes the output speed in miles per hour
|
||||
by multiplying the measured speed in kilometers per hour by 0.62, and the
|
||||
miles per hour values in a <code>.xml</code> file are thus only accurate to
|
||||
two significant figures, even though they're printed out to three decimal
|
||||
places. Because of this limitation, the sequence <code>ptpk</code>,
|
||||
<code>ptunpk</code> is not quite the identity function; in particular, the
|
||||
wattage values from <code>ptpk</code> may only be accurate to two significant
|
||||
digits.
|
||||
|
||||
@@ -11,8 +11,22 @@ used, and Sean Rhea coded up their combined discoveries into the two
|
||||
utilities, <code>ptdl</code> and <code>ptunpk</code>.
|
||||
|
||||
<p>
|
||||
Rob Carlsen helped get the serial port version of the PowerTap Pro working
|
||||
with the Keyspan USB-to-serial adaptor. Scott Overfield helped me figure out
|
||||
that we should be using the <code>/dev/cu.*</code> devices instead of the
|
||||
<code>/dev/tty.*</code> ones.
|
||||
Later that year, Sean needed to learn QT for his real job, and he set about
|
||||
writing a graphical version of <code>ptdl</code> and <code>ptunpk</code> for
|
||||
practice. He released the first graphical version on September 6, 2006,
|
||||
changing the name to GoldenCheetah in reference to an old legend from his days
|
||||
as a runner.
|
||||
|
||||
<p>
|
||||
Since then, a number of others have helped out in various ways.
|
||||
Robert Carlsen helped get the serial port version of the PowerTap Pro working
|
||||
with the Keyspan USB-to-serial adaptor. Robert also figured out how to build
|
||||
universal binaries for Mac OS X. Scott Overfield helped figure out
|
||||
that we should be using the <code>/dev/cu.*</code> devices instead of the
|
||||
<code>/dev/tty.*</code> ones. Aldy Hernandez and Andrew Kruse helped get
|
||||
things working under Linux.
|
||||
Dan Connelly helped find and fix several core dumps.
|
||||
Justin Knotzke contributed code to import comma-separated value files
|
||||
and visually mark intervals on the ride plot. J.T. Conklin added the ability
|
||||
to import TCX files. J.T. also added a pedal force vs. pedal velocity chart. Tom Montgomery added IBike 3 support and cleaned up the Import CSV tool, adding the default ride date when importing new files. Ned Harding resurrected the Windows builds of Golden Cheetah (fixing USB download inconsistencies in the process), then added slew of functions including the long-desired Split Ride feature. (available in version 1.0.305+).
|
||||
|
||||
|
||||
BIN
doc/critical-power.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
@@ -1,7 +1,14 @@
|
||||
<!-- $Id: download.content,v 1.6 2006/08/11 20:21:03 srhea Exp $ -->
|
||||
<!-- $Id: download.content,v 1.6 2009/01/09 20:45:03 rcarlsen Exp $ -->
|
||||
|
||||
Right now Golden Cheetah is available as source code and in binary form for
|
||||
Mac OS X on PowerPC processors.
|
||||
<p>
|
||||
Golden Cheetah is available as source code and in binary form for
|
||||
Mac OS X Universal Binary, Linux on x86 processors and Windows 32-bit.
|
||||
</p>
|
||||
<p>
|
||||
Depending on your operating system, you may need to install the <a
|
||||
href="http://www.ftdichip.com/Drivers/D2XX.htm">FTDI USB
|
||||
driver</a> if you're using the PowerTap's new USB download cradle. The FTDI USB drivers are an optional install if you do not plan on downloading from your device using Golden Cheetah.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<font face="arial,helvetica,sanserif">
|
||||
@@ -10,15 +17,20 @@ Mac OS X on PowerPC processors.
|
||||
|
||||
<p>
|
||||
The Golden Cheetah source code is available via
|
||||
<a href="http://subversion.tigris.org/">Subversion</a>.
|
||||
<a href="http://git-scm.com/">git</a>. You need to install
|
||||
<a href="http://www.trolltech.com/products/qt">QT 4.5.x</a> and
|
||||
<a href="http://qwt.sourceforge.net/">qwt 5.0.2</a> (as a static, not
|
||||
dynamic, library) and
|
||||
edit <code>goldencheetah/src/src.pro</code> to point to them
|
||||
before building GoldenCheetah.
|
||||
Use this command to check out the current version of the repository:
|
||||
|
||||
<pre>
|
||||
svn checkout http://goldencheetah.org/svn/trunk goldencheetah
|
||||
git clone git://github.com/srhea/GoldenCheetah.git
|
||||
</pre>
|
||||
|
||||
You can also browse the source <a
|
||||
href="http://goldencheetah.org/svn/trunk">here</a>.
|
||||
You can also <a href="http://github.com/srhea/GoldenCheetah/tree/master/">browse
|
||||
the source on github</a>.
|
||||
|
||||
<p>
|
||||
<font face="arial,helvetica,sanserif">
|
||||
@@ -27,13 +39,178 @@ href="http://goldencheetah.org/svn/trunk">here</a>.
|
||||
|
||||
<p>
|
||||
<center>
|
||||
<table width="100%">
|
||||
<table width="100%" cellspacing="10">
|
||||
<tr>
|
||||
<td width="20%"><i>Date</i></td>
|
||||
<td width="30%"><i>File</i></td>
|
||||
<td width="20%"><i>Version</i></td>
|
||||
<td width="30%"><i>Files</i></td>
|
||||
<td><i>Description</i></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">1.1.325</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_1.1.325_Linux_x86.gz">Linux x86</a><br>
|
||||
<a href="GoldenCheetah_1.1.325_Linux_x86_64.gz">Linux x86_64</a><br>
|
||||
<a href="GoldenCheetah_1.1.325_Darwin_Universal.dmg">Mac OS X Universal</a><br>
|
||||
<a href="GoldenCheetah_1.1.325_Windows_Installer.exe">Windows 32-bit</a>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p>
|
||||
First official Windows release courtesy of Ned Harding. Ned put much effort into the port to make the download reliable and created a nice installer, too (Thanks Ned!). He also provided the long-awaited Split Ride feature - break up a ride file into separate rides easily using long time gaps and intervals.
|
||||
<ul>
|
||||
<li>Ant+Sport PowerTap support.
|
||||
<li>Split Rides by time gaps or intervals.
|
||||
<li>Delete ride from list.
|
||||
<li>Use distance or time for x-axis in Ride Plot (Thanks Damain).
|
||||
<li>Numerous bug fixes (Thanks Tom, Dan).
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">1.0.277</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_1.0.277_Linux_x86.gz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_1.0.277_Linux_x86_64.tar.gz">Linux x86_64</a>,<br>
|
||||
<a href="GoldenCheetah_1.0.277_Darwin_Universal.dmg">Mac OS X Universal</a>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p>*Note: Beginning with this release we are changing to a numbered versioning system. Minor point releases will generally indicate builds with new features, while bugfix releases will increment the final number, which represents the svn revision*</p>
|
||||
<p>Several new features in this release: Critical Power calculator, find best intervals utility, Pedal Force / Pedal Velocity chart, iBike and Ergomo CSV import, GUI power zones creator, separate vertical axes for Power / HR / Cadence and Speed in the Ride plot, sorting rides with the most recent at the top of the list, and many bug fixes courtesy of JT Conklin.
|
||||
</p>
|
||||
<p>You may need to install <a href="http://www.ftdichip.com/Drivers/D2XX.htm">USB drivers</a> from FTDI.
|
||||
</p>
|
||||
<p>
|
||||
For posterity, the <a href="http://robertcarlsen.net/blog/?page_id=49">beta version</a> for Windows, based on r295.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Mar 10, 2008</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2008-03-10_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2008-03-10_Darwin_Universal.dmg">Mac OS X Universal</a>
|
||||
</td>
|
||||
<td valign="top">
|
||||
This release introduces <a href="http://www.physfarm.com/Analysis%20of%20Power%20Output%20and%20Training%20Stress%20in%20Cyclists-%20BikeScore.pdf">BikeScore™</a>,
|
||||
a metric of training stress developed by Dr. Philip Skiba. It also
|
||||
fixes several small bugs in earlier releases.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Sep 23, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-09-23_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-09-23_Darwin_i386.dmg">Mac OS X x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-09-23_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">
|
||||
Bug fix release. CVS imports weren't quite working in the last one.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Sep 18, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-09-18_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-09-18_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">
|
||||
This release adds two small, but excellent features from Justin Knotzke:
|
||||
CSV file imports and visual interval markers in the ride plot.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Aug 7, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-08-07_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-08-07_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">This release fixes a bug in the critical power
|
||||
intervals graph where you could get bad data if you started an interval
|
||||
after a long period of not moving. It also adds really basic zooming to
|
||||
the ride plot: use the left mouse button to zoom in and the right one to
|
||||
return to the previous zoom state. It's pretty crappy right now, but
|
||||
it's better than nothing.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Apr 26, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-04-26_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-04-26_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">This release fixes some bugs and adds a whole bunch of
|
||||
new features:
|
||||
<ul>
|
||||
<li>Now imports .srm files (direct download from SRM hopefully coming
|
||||
soon)
|
||||
<li>New "Weekly Summary" tab shows total weekly hours, miles, and work
|
||||
<li>Power zones can now be entered into a text file, after which GC will
|
||||
display time in each zone in the ride and weekly summaries; for more
|
||||
information on the zone file format, <a href="zones.html">see this
|
||||
page</a>.
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Apr 1, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-04-01_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-04-01_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">This release fixes a bug that was introduced with the
|
||||
hardware echo detection code. If you're using the CycleOps-supplied USB
|
||||
cable to download from your PowerTap unit, this release should make
|
||||
downloads more reliable. (Those using the KeySpan USB-to-serial adaptor
|
||||
or a plain-old serial port shouldn't see any difference.)</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Feb 22, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-02-22_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-02-22_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">Clicking on the Critical Power Plot now displays the
|
||||
interval duration, maximum power for that ride, and maximum power for all
|
||||
rides below the plot. Also fixes a bug for recording intervals longer than
|
||||
two seconds.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Feb 12, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-02-12_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-02-12_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">Interval information now included in ride summary, rides can
|
||||
now be exported as comma-separated values for import into Excel, and better
|
||||
automatic detection of hardware echo. Also includes a number of bux
|
||||
fixes.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Jan 30, 2007</td>
|
||||
<td valign="top">
|
||||
<a href="GoldenCheetah_2007-01-30_Linux_x86.tgz">Linux x86</a>,<br>
|
||||
<a href="GoldenCheetah_2007-01-30_Darwin_powerpc.dmg">Mac OS X PowerPC</a>
|
||||
</td>
|
||||
<td valign="top">Bug fix release.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Jan 6, 2007</td>
|
||||
<td valign="top"><a href="GoldenCheetah_2007-01-06_Linux_x86.tgz">Linux
|
||||
x86</a></td>
|
||||
<td valign="top">First release for Linux.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Dec 25, 2006</td>
|
||||
<td valign="top"><a href="GoldenCheetah_2006-12-25_Darwin_powerpc.dmg">Mac OS
|
||||
@@ -84,7 +261,7 @@ These are the older, source-only, command-line distributions. I've left them
|
||||
up for historical purposes only; I don't recommend using them.
|
||||
|
||||
<center>
|
||||
<table width="100%">
|
||||
<table width="100%" cellspacing="10">
|
||||
<tr>
|
||||
<td width="20%"><i>Date</i></td>
|
||||
<td width="30%"><i>File</i></td>
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
<!-- $Id: faq.content,v 1.4 2006/07/05 16:59:56 srhea Exp $ -->
|
||||
|
||||
<p>
|
||||
<i>I downloaded a .dmg, opened it, dragged and dropped GoldenCheetah into
|
||||
Applications, double-clicked on it, and nothing happened. What gives?</i>
|
||||
<b><i>GoldenCheetah doesn't find my PowerTap on Ubuntu Linux.</i></b>
|
||||
<p>
|
||||
If you're using the USB cradle (as opposed to the older, serial one),
|
||||
the FTDI driver sometimes conflicts with the braille terminal in the
|
||||
default Ubuntu installation. Try unplugging the PT cradle from the
|
||||
computer and uninstalling <code>brltty</code>:
|
||||
<blockquote>
|
||||
<code>sudo apt-get remove brltty</code>
|
||||
</blockquote>
|
||||
Then plug the device back in and it should work.
|
||||
|
||||
<p>
|
||||
<b><i>I downloaded a .dmg, opened it, dragged and dropped GoldenCheetah into
|
||||
Applications, double-clicked on it, and nothing happened. What
|
||||
gives?</i></b>
|
||||
|
||||
<p>
|
||||
Are you running OS X Tiger? You need to be. If you are, and you're still
|
||||
@@ -17,7 +30,8 @@ then press <return> and send an email to the mailing list with
|
||||
whatever it prints out. We'll help you debug it.
|
||||
|
||||
<p>
|
||||
<i>I've downloaded and unpacked the data. Now what do I do with it?</i>
|
||||
<b><i>I've downloaded and unpacked the data. Now what do I do with
|
||||
it?</i></b>
|
||||
|
||||
<p>
|
||||
We highly recommend that you buy and read both Joe Friel's <i>The
|
||||
@@ -44,8 +58,8 @@ marginheight="0" frameborder="0"></iframe>
|
||||
</center>
|
||||
|
||||
<p>
|
||||
<i>Does the output of <code>ptunpk</code> exactly match that of the software
|
||||
included with the PowerTap?</i>
|
||||
<b><i>Does the output of <code>ptunpk</code> exactly match that of the software
|
||||
included with the PowerTap?</i></b>
|
||||
|
||||
<p>
|
||||
Almost. If you run it in compatibility mode, using the <code>-c</code>
|
||||
|
||||
@@ -16,7 +16,7 @@ open (FILE, "$content_file") or die "Could not open $content_file";
|
||||
print<<EOF;
|
||||
<!--
|
||||
|
||||
Copyright (c) 2006 Sean C. Rhea (srhea\@srhea.net)
|
||||
Copyright (c) 2006-2008 Sean C. Rhea (srhea\@srhea.net)
|
||||
All rights reserved.
|
||||
|
||||
This file was automatically generated by genpage.pl. To change it,
|
||||
@@ -26,8 +26,8 @@ print<<EOF;
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Golden Cheetah: PowerTap Software for Mac OS X</title>
|
||||
<meta name="keywords" content="powertap mac cycling performance">
|
||||
<title>Golden Cheetah: Cycling Performance Software for Linux, Mac OS X, and Windows</title>
|
||||
<meta name="keywords" content="powertap srm linux mac cycling performance">
|
||||
</head>
|
||||
|
||||
<body text="#000000"
|
||||
@@ -48,6 +48,7 @@ print<<EOF;
|
||||
<br> <b><a href="screenshots.html">Screenshots</a>
|
||||
<br> <b><a href="users-guide.html">User's Guide</a>
|
||||
<br> <b><a href="faq.html">FAQ</a>
|
||||
<br> <b><a href="wishlist.html">Wish List</a>
|
||||
<br> <b><a href="license.html">License</a></b>
|
||||
<br> <b><a href="download.html">Download</a></b>
|
||||
<br> <b><a href="contrib.html">Contributors</a></b>
|
||||
@@ -87,7 +88,7 @@ google_color_text = "000000";
|
||||
Cheetah</font></b></big></big></big>
|
||||
<br>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
PowerTap Software for Mac OS X
|
||||
Cycling Performance Software for Linux, Mac OS X, and Windows
|
||||
</font></big>
|
||||
<p>
|
||||
</td></tr>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<!-- $Id: index.content,v 1.1 2006/05/16 14:24:50 srhea Exp $ -->
|
||||
|
||||
<p>
|
||||
The goal of the Golden Cheetah project is to develop a software package that:
|
||||
GoldenCheetah is a software package that:
|
||||
|
||||
<ul>
|
||||
<li>Downloads ride data from power measurement devices, such as the <a
|
||||
href="http://www.cycleops.com/products/powertap.htm">CycleOps PowerTap</a>,
|
||||
the <a href="http://www.ergomo.net/Home-_14.html">ergomo</a>, the <a
|
||||
href="http://www.polarusa.com/consumer/powerkit/default.asp">Polar
|
||||
Electro</a>, and the <a href="http://www.srm.de/usa/index.html">SRM Training
|
||||
System</a><p>
|
||||
<li>Helps athletes analyze downloaded data with features akin to commercial
|
||||
power analysis software, such as <a href="http://cyclingpeaks.com/">Cycling
|
||||
Peaks</a><p>
|
||||
<li>Works on non-Microsoft Windows-based systems, such as FreeBSD, Linux, and
|
||||
Mac OS X<p>
|
||||
<li>Is available under an
|
||||
<a href="http://www.opensource.org/docs/definition.php">Open Source</a>
|
||||
license
|
||||
<li>Downloads ride data directly from the CycleOps PowerTap, including the
|
||||
newer Ant+Sport models.<p>
|
||||
|
||||
<li>Imports ride data from SRM, Garmin TCX, Polar HRM, and CSV files,
|
||||
including those from Cycling Peaks (TM) and the ergomo.<p>
|
||||
|
||||
<li>Provides a rich set of analysis tools, including critial power,
|
||||
BikeScore (TM), power histograms, a best interval finder, and pedal force
|
||||
versus pedal velocity, to name just a few.<p>
|
||||
|
||||
<li>Works identically under Linux, Mac OS X, and Windows.<p>
|
||||
|
||||
<li>Is available under an Open Source license.
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
In short, we believe that cyclists should be able to download their power data
|
||||
We believe that cyclists should be able to download their power data
|
||||
to the computer of their choice, analyze it in whatever way they see fit, and
|
||||
share their methods of analysis with others.
|
||||
|
||||
|
||||
BIN
doc/main-window.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
# $Id: mkdmg.sh,v 1.2 2006/09/06 23:23:03 srhea Exp $
|
||||
export PATH=/usr/local/Trolltech/Qt-4.1.1-static/bin:$PATH
|
||||
VERS=`date +'%Y-%m-%d'`
|
||||
OS=`uname -s`
|
||||
CPU=`uname -p`
|
||||
@@ -7,22 +8,24 @@ SUFFIX="$VERS"_"$OS"_"$CPU"
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
svn checkout svn+ssh://goldencheetah.org/home/srhea/svnroot/goldencheetah/trunk goldencheetah
|
||||
svn checkout svn+ssh://goldencheetah.org/home/srhea/svnroot/goldencheetah/trunk/src goldencheetah
|
||||
cd goldencheetah
|
||||
qmake
|
||||
make
|
||||
mv src/gui/GoldenCheetah.app ..
|
||||
make clean
|
||||
rm doc/gc_*.tgz
|
||||
rm doc/GoldenCheetah_*.dmg
|
||||
mv gui/GoldenCheetah.app ..
|
||||
#make clean
|
||||
#rm doc/gc_*.tgz
|
||||
#rm doc/GoldenCheetah_*.dmg
|
||||
#rm doc/GoldenCheetah_*.tgz
|
||||
cd ..
|
||||
strip GoldenCheetah.app/Contents/MacOS/GoldenCheetah
|
||||
find . -name .svn | xargs rm -rf
|
||||
tar czvf src.tgz goldencheetah
|
||||
rm -rf goldencheetah
|
||||
SIZE=`du -csk * | grep total | awk '{printf "%.0fm", $1/1024+5}'`
|
||||
strip GoldenCheetah.app/Contents/MacOS/GoldenCheetah
|
||||
#find . -name .svn | xargs rm -rf
|
||||
#tar czvf src.tgz goldencheetah
|
||||
SIZE=`du -csk GoldenCheetah.app | grep total | awk '{printf "%.0fm", $1/1024+5}'`
|
||||
hdiutil create -size $SIZE -fs HFS+ -volname "Golden Cheetah $VERS" tmp.dmg
|
||||
hdiutil attach tmp.dmg
|
||||
cp -R GoldenCheetah.app src.tgz /Volumes/Golden\ Cheetah\ $VERS/
|
||||
cp -R GoldenCheetah.app /Volumes/Golden\ Cheetah\ $VERS/
|
||||
hdiutil detach /Volumes/Golden\ Cheetah\ $VERS/
|
||||
hdiutil convert tmp.dmg -format UDZO -o GoldenCheetah_$SUFFIX.dmg
|
||||
hdiutil internet-enable -yes GoldenCheetah_$SUFFIX.dmg
|
||||
|
||||
27
doc/power.zones
Normal file
@@ -0,0 +1,27 @@
|
||||
From BEGIN until 2006/07/17, CP=297:
|
||||
1, Active Recovery, 122, 167
|
||||
2, Endurance, 167, 228
|
||||
3, Tempo, 228, 274
|
||||
4, Lactate Threshold, 274, 319
|
||||
5, VO2 Max, 319, 365
|
||||
6, Anaerobic Capacity, 365, 678
|
||||
7, Sprinting, 678, MAX
|
||||
|
||||
From 2006/07/17 until 2007/02/05, CP=329:
|
||||
1, Active Recovery, 135, 185
|
||||
2, Endurance, 185, 253
|
||||
3, Tempo, 253, 303
|
||||
4, Lactate Threshold, 303, 354
|
||||
5, VO2 Max, 354, 404
|
||||
6, Anaerobic Capacity, 404, 752
|
||||
7, Sprinting, 752, MAX
|
||||
|
||||
From 2007/02/05 until END, CP=347:
|
||||
1, Active Recovery, 139, 191
|
||||
2, Endurance, 191, 260
|
||||
3, Tempo, 260, 312
|
||||
4, Lactate Threshold, 312, 364
|
||||
5, VO2 Max, 364, 416
|
||||
6, Anaerobic Capacity, 416, 774
|
||||
7, Sprinting, 774, MAX
|
||||
|
||||
BIN
doc/screenshot-download.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 81 KiB |
BIN
doc/screenshot-weekly.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
@@ -3,6 +3,13 @@
|
||||
<p>
|
||||
<center>
|
||||
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
The Download Dialog
|
||||
</font></big>
|
||||
<p>
|
||||
<img src="screenshot-download.png" alt="Download Dialog" align="center">
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
The Ride Summary
|
||||
</font></big>
|
||||
@@ -30,6 +37,13 @@ The Power Histogram
|
||||
<p>
|
||||
<img src="screenshot-phist.png" alt="The Power Histogram" align="center">
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
The Weekly Summary
|
||||
</font></big>
|
||||
<p>
|
||||
<img src="screenshot-weekly.png" alt="The Weekly Summary" align="center">
|
||||
|
||||
</center>
|
||||
|
||||
|
||||
|
||||
@@ -1,241 +1,233 @@
|
||||
<!-- $Id: users-guide.content,v 1.5 2006/05/27 16:32:46 srhea Exp $ -->
|
||||
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Using the GUI
|
||||
Step 1: Installing the FTDI drivers
|
||||
</font></big>
|
||||
|
||||
<p>
|
||||
Using the graphical version of Golden Cheetah should be pretty
|
||||
self-explanatory. Download the disk image from the <a
|
||||
href="download.html">download page</a>, drag the Golden Cheetah application
|
||||
into your Applications folder, open your Applications folder, and then double
|
||||
click on Golden Cheetah.
|
||||
Depending on your operating system, you may need to install the <a
|
||||
href="http://www.ftdichip.com/Drivers/D2XX.htm">FTDI USB
|
||||
driver</a> if you're using the PowerTap's new USB download cradle. The FTDI USB drivers are an optional install if you do not plan on downloading from your device using Golden Cheetah.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you're running Linux, you may also need to uninstall the <code>brtty</code>
|
||||
(Braille TTY) application, as it interferes with FTDI's driver. The command
|
||||
|
||||
<pre>
|
||||
sudo apt-get remove brtty
|
||||
</pre>
|
||||
|
||||
should do the trick on Debian/Ubuntu.
|
||||
|
||||
<p>
|
||||
The latest version (7.1.1) of Saris's PowerAgent software uses an incompatible
|
||||
version of FTDI's driver from the one GoldenCheetah uses, and PowerAgent
|
||||
removes the driver that GoldenCheetah needs when you install PowerAgent. If
|
||||
you want to run both GoldenCheetah and PowerAgent, you need to use PowerAgent
|
||||
version 7.0.1 or earlier. We're working to correct this problem, but we're
|
||||
not there yet.
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Using the Command Line Utilities
|
||||
Step 2: Installing GoldenCheetah
|
||||
</font></big>
|
||||
|
||||
<p>
|
||||
In addition to the GUI, Golden Cheetah comes with
|
||||
several command line utilities:
|
||||
<code>ptdl</code>, which downloads ride data from a PowerTap Pro version 2.21
|
||||
cycling computer, <code>ptunpk</code>, which unpacks the raw bytes downloaded
|
||||
by <code>ptdl</code> and outputs more human-friendly ride information, and
|
||||
<code>cpint</code>, which computes your critical power (see below). All three
|
||||
are written in simple C code but have only been tested on Mac OS X so far.
|
||||
We've also written several Perl scripts to help you graph and summarize the
|
||||
data.
|
||||
To install GoldenCheetah, go to <a href="download.html">the download page</a>
|
||||
and download the version for your operating system and processor.
|
||||
|
||||
<p>
|
||||
On Mac OS X, when the download finishes, Mac OS X should automatically open
|
||||
the <code>.dmg</code> file for you. If not, double-click to open it. Drag
|
||||
the GoldenCheetah icon into your Applications folder, and you're done.
|
||||
|
||||
<p>
|
||||
The Linux version of GoldenCheetah is distributed as a tarball. Download this
|
||||
file and save it to <code>/tmp</code>, then from a terminal:
|
||||
|
||||
<pre>
|
||||
cd /tmp
|
||||
tar xzvf GoldenCheetah_DATE_Linux_x86.tgz
|
||||
cd GoldenCheetah_DATE_Linux_x86
|
||||
sudo cp GoldenCheetah /usr/local/bin
|
||||
cd ..
|
||||
rm -rf GoldenCheetah_DATE_Linux_x86.tgz
|
||||
</pre>
|
||||
|
||||
Be sure to replace "DATE" with the date of the revision you downloaded, such
|
||||
as "2007-09-23".
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Extracting the Data
|
||||
Step 3: Running GoldenCheetah
|
||||
</font></big>
|
||||
<p>
|
||||
To use <code>ptdl</code>, you'll first need to install
|
||||
<a href="http://www.ftdichip.com/Drivers/VCP.htm">the drivers</a> for the
|
||||
FTDI chip the PowerTap Pro USB Downloader uses. Once these are installed, you
|
||||
should be able to just run <code>ptdl</code> without arguments:
|
||||
|
||||
<pre>
|
||||
$ ./ptdl
|
||||
Reading from /dev/tty.usbserial-3B1.
|
||||
Reading version information...done.
|
||||
Reading ride time...done.
|
||||
Writing to 2006_05_15_11_34_03.raw.
|
||||
Reading ride data..............done.
|
||||
$ head -5 2006_05_15_11_34_03.raw
|
||||
57 56 55 64 02 15
|
||||
60 06 05 0f 6b 22
|
||||
40 08 30 00 00 00
|
||||
86 0e 74 99 00 55
|
||||
81 06 77 a8 40 55
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
If everything goes well, <code>ptdl</code> will automatically detect the
|
||||
device (<code>/dev/tty.usbserial-3B1</code> in the example above), read the
|
||||
ride data from it, and write to a file named by the date and time at which the
|
||||
ride started (<code>2006_05_15_11_34_03.raw</code> in the example; the format
|
||||
is YYYY_MM_DD_hh_mm_ss.raw).
|
||||
To run GoldenCheetah on Mac OS X, double-click on the GoldenCheetah icon in
|
||||
your Applications folder. On Linux, just type "GoldenCheetah" at the prompt.
|
||||
|
||||
<p>
|
||||
The first time you run GoldenCheetah, you'll get an empty "Choose a Cyclist"
|
||||
dialog box:
|
||||
|
||||
<p>
|
||||
<center><img src="choose-a-cyclist.png"></center>
|
||||
|
||||
<p>
|
||||
Click on "New...", enter your name and click "OK", then select your name and
|
||||
click "Open". After that, the main GoldenCheetah window will open:
|
||||
|
||||
<p>
|
||||
<center><img src="main-window.png"></center>
|
||||
|
||||
<p>
|
||||
Your main window won't yet have any rides in it, of course. To fix that, you
|
||||
need either to download a ride from your PowerTap or import one from another
|
||||
program. GoldenCheetah can import <code>.srm</code> files recorded on SRM
|
||||
power meters and <code>.csv</code> files created by other programs. To
|
||||
download a file from your PowerTap, select "Ride->Download from device..."
|
||||
from the menu. To import one, select either "Ride->Import from SRM..." or
|
||||
"Ride->Import from CSV...".
|
||||
|
||||
<p>
|
||||
Once you've downloaded or imported a ride, you can see some simple statistics
|
||||
about it on the "Ride Summary" page: your total riding time and average power,
|
||||
for example. If you click on the "Ride Plot" tab at the top of the screen,
|
||||
you can see a graph of your speed, power, cadence, and heart rate during the
|
||||
ride. The "Power Histogram" shows how much time you spent at each power
|
||||
during the ride, and the "Notes" tab allows you to record notes about the
|
||||
ride. The "Weekly Summary" shows your total time and work for the week.
|
||||
|
||||
<p>
|
||||
The "Critical Power Plot" is one of the most useful features of GoldenCheetah.
|
||||
It shows the highest average power you attained for every interval length
|
||||
during the ride. Some people call this the "Mean Maximal Power" graph. The
|
||||
green line shows values for this ride; the red line shows the combination of
|
||||
all your rides. (If you only have one ride so far, the two lines will
|
||||
overlap.) Clicking on the graph with your mouse brings up a blue line, and
|
||||
the values under this line are shown at the bottom of the screen.
|
||||
|
||||
<p>
|
||||
It helps to think about an example:
|
||||
|
||||
<p>
|
||||
<center><img src="critical-power.png"></center>
|
||||
|
||||
<p>
|
||||
In this example, the blue line is right around the 14-second mark on the
|
||||
x-axis. So the values shown under "Today" and "All Rides", at the bottom, are
|
||||
the hardest the cyclist went for any 14-second period during the ride itself
|
||||
and during all rides he's ever recorded in GoldenCheetah. Since the two
|
||||
values are the same, he set a new personal record during this ride.
|
||||
|
||||
<p>
|
||||
The Critical Power Plot is most useful before you're going to go do intervals
|
||||
or a time trial. Say you want to do six 2-minute intervals with three minutes
|
||||
rest in between. Click on the Critical Power Plot, drag the blue line to the
|
||||
2-minute mark, and read the value shown in "All Rides". That's the hardest
|
||||
you've ever gone for two minutes. Now go out and try to beat it!
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Unpacking the Data
|
||||
Step 4: Setting Up Your Power Zones
|
||||
</font></big>
|
||||
<p>As shown by the <code>head</code> command above, the data in this
|
||||
<code>.raw</code> file is just the raw bytes that represent your ride. To
|
||||
unpack those bytes and display them in a more human-friendly format, use
|
||||
<code>ptunpk</code>:
|
||||
|
||||
<p>
|
||||
If you look back at the screenshot above, you may notice that there are
|
||||
several things shown in the "Ride Summary" tab that aren't on your version.
|
||||
The picture above shows a non-zero "Bike Score", and there's a list of how
|
||||
much time the cyclist spent in each "Power Zone" during the ride as well.
|
||||
|
||||
<p>
|
||||
BikeScore(TM) is a measure of the physiological stress you underwent during a
|
||||
ride. It was developed by Dr. Philip Skiba, and you can read more about it in
|
||||
<a href="http://www.physfarm.com/Analysis%20of%20Power%20Output%20and%20Training%20Stress%20in%20Cyclists-%20BikeScore.pdf">an article he wrote</a>.
|
||||
|
||||
<p>
|
||||
For GoldenCheetah to compute your BikeScore and the time spent in each power
|
||||
zone, you first need to tell it what your power zones and critical power
|
||||
are. You can define your power zones however you like, maybe using the ones
|
||||
defined by Joe Friel, for example. Your critical power should be the
|
||||
maximum power you can sustain over an hour. Some people call this your
|
||||
"lactate threshold" or "functional threshold power". Our friend Bill says a
|
||||
rose by any other name would smell as sweet.
|
||||
|
||||
<p>
|
||||
We'll have a dialog box that will let you set up your power zones and
|
||||
critical power in a future version of GoldenCheetah, but for now you'll need
|
||||
to use a text editor. On Linux, that probably means nano, vi, or emacs.
|
||||
On Mac, the easiest editor to use is TextEdit, which is in your Applications
|
||||
folder.
|
||||
|
||||
<p>
|
||||
Start by
|
||||
downloading <a href="power.zones">this sample file</a> and saving it in
|
||||
|
||||
<pre>
|
||||
$ ./ptunpk 2006_05_15_11_34_03.raw
|
||||
$ head -5 2006_05_15_11_34_03.dat
|
||||
# Time Torq MPH Watts Miles Cad HR Int
|
||||
# 2006/5/15 11:34:03 1147707243
|
||||
# wheel size=2096 mm, interval=0, rec int=1
|
||||
0.021 13.1 2.450 43 0.00781 0 85 0
|
||||
0.042 13.4 5.374 97 0.00912 64 85 0
|
||||
~/Library/GoldenCheetah/Your Name/power.zones
|
||||
</pre>
|
||||
|
||||
<code>ptunpk</code> takes a <code>.raw</code> file for input and writes a
|
||||
<code>.dat</code> file as output. Lines that start with an ampersand ("#") in
|
||||
this file are comments; the other lines represent measured samples. As shown
|
||||
by the first comment in the file, the columns are: time in minutes, torque in
|
||||
Newton-meters, speed in miles per hour, power in watts, distance in miles,
|
||||
cadence, heart rate, and interval number.
|
||||
<p>
|
||||
where "~" is your home directory (e.g., <code>/Users/srhea</code> on Mac or
|
||||
<code>/home/srhea</code> on Linux) and "Your Name" is the name you chose when
|
||||
you first opened GoldenCheetah. Open the power.zones file in a text editor
|
||||
and you'll see this:
|
||||
|
||||
<blockquote>
|
||||
<pre>
|
||||
From BEGIN until 2006/07/17, CP=297:
|
||||
1, Active Recovery, 122, 167
|
||||
2, Endurance, 167, 228
|
||||
3, Tempo, 228, 274
|
||||
4, Lactate Threshold, 274, 319
|
||||
5, VO2 Max, 319, 365
|
||||
6, Anaerobic Capacity, 365, 678
|
||||
7, Sprinting, 678, MAX
|
||||
|
||||
From 2006/07/17 until 2007/02/05, CP=329:
|
||||
1, Active Recovery, 135, 185
|
||||
2, Endurance, 185, 253
|
||||
3, Tempo, 253, 303
|
||||
4, Lactate Threshold, 303, 354
|
||||
5, VO2 Max, 354, 404
|
||||
6, Anaerobic Capacity, 404, 752
|
||||
7, Sprinting, 752, MAX
|
||||
|
||||
From 2007/02/05 until END, CP=347:
|
||||
1, Active Recovery, 139, 191
|
||||
2, Endurance, 191, 260
|
||||
3, Tempo, 260, 312
|
||||
4, Lactate Threshold, 312, 364
|
||||
5, VO2 Max, 364, 416
|
||||
6, Anaerobic Capacity, 416, 774
|
||||
7, Sprinting, 774, MAX
|
||||
</pre>
|
||||
</blockquote>
|
||||
|
||||
<p>
|
||||
The format of the file is simple. You define a range of time, starting with a
|
||||
date or "BEGIN" to indicate the oldest possible time and ending with a date or
|
||||
"END" to indicate the latest possible time. Then you put your critical power
|
||||
(CP) for that date range. Then you list your zones, where each zone has a
|
||||
number, a name, a minimum power value, and a maximum power value. You can
|
||||
have as many time ranges and zones as you like. Most people enter a new time
|
||||
range every time their critical power goes up--right after a fitness test, for
|
||||
example.
|
||||
|
||||
<p>
|
||||
NOTE: By default, Mac OS's TextEdit will try and save the power.zones file
|
||||
with a <code>.txt</code> extension. Use the menu command "Format->Make Plain
|
||||
Text" to get it to let you save the file with a <code>.zones</code> extension
|
||||
instead.
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Summarizing the Data
|
||||
</font></big>
|
||||
<p>
|
||||
We hope to have a graphical interface to these programs soon, but until then,
|
||||
the only summarization tools we have are command-line programs. The script
|
||||
<code>intervals.pl</code> summarizes the intervals performed in a workout:
|
||||
|
||||
<small>
|
||||
<pre>
|
||||
$ ./intervals.pl 2006_05_03_16_24_04.dat
|
||||
Power Heart Rate Cadence Speed
|
||||
Int Dur Dist Avg Max Avg Max Avg Max Avg Max
|
||||
0 77:10 19.3 213 693 134 167 82 141 16.0 27.8
|
||||
1 4:03 0.9 433 728 175 203 84 122 13.0 18.8
|
||||
2 7:23 1.0 86 502 135 179 71 141 16.0 28.2
|
||||
3 4:27 0.9 390 628 170 181 70 100 12.0 17.6
|
||||
4 8:04 0.9 60 203 130 178 50 120 18.0 30.1
|
||||
5 4:30 0.9 384 682 170 179 79 113 11.0 18.6
|
||||
6 8:51 1.1 53 245 125 176 70 141 8.0 26.6
|
||||
7 2:48 0.4 400 614 164 178 62 91 8.0 13.6
|
||||
8 7:01 1.1 46 268 128 170 71 141 12.0 28.8
|
||||
9 4:30 0.9 379 560 168 180 81 170 11.0 18.3
|
||||
10 28:46 6.5 120 409 128 179 79 141 15.0 31.0
|
||||
</pre>
|
||||
</small>
|
||||
|
||||
<p>
|
||||
In the example above, a rider performed five hill intervals, four of which
|
||||
climbed a medium size hill that took about 4-5 minutes to climb (intervals
|
||||
1, 3, 5, and 9), and one on a shorter hill that took just under 3 minutes to
|
||||
climb (interval 7).
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Graphing the Data
|
||||
</font></big>
|
||||
<p>
|
||||
For graphing the data in the ride, we use <code>smooth.pl</code> and the
|
||||
<code>gnuplot</code> program. You can use <a href="sample.gp">sample.gp</a>
|
||||
to graph the power, heart rate, cadence, and speed for the hill workout above:
|
||||
|
||||
<pre>
|
||||
$ gnuplot sample.gp
|
||||
</pre>
|
||||
|
||||
<img align="center" alt="Sample Plot" src="sample.png">
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Finding Your "Critical Power"
|
||||
</font></big>
|
||||
<p>
|
||||
Joe Friel calls the maximum average power a rider can sustain over an interval
|
||||
the rider's "critical power" for that duration. The <code>cpint</code>
|
||||
program automatically computes your critical power over all interval lengths
|
||||
using the data from all your past rides. This program looks at all the
|
||||
<code>.raw</code> files in a directory, calculating your maximum power over
|
||||
every subinterval length and storing them in a corresponding <code>.cpi</code>
|
||||
file. It then combines the data in all of the <code>.cpi</code> files to find
|
||||
your critical power over <i>all</i> subintervals of <i>all</i> your rides.
|
||||
|
||||
<pre>
|
||||
$ ls *.raw
|
||||
2006_04_28_10_48_33.raw 2006_05_10_17_08_30.raw 2006_05_18_16_32_53.raw
|
||||
2006_05_03_16_24_04.raw 2006_05_13_10_29_12.raw 2006_05_21_12_25_07.raw
|
||||
2006_05_05_10_52_05.raw 2006_05_15_11_34_03.raw 2006_05_22_18_28_47.raw
|
||||
...
|
||||
2006_05_09_09_54_29.raw 2006_05_17_16_44_35.raw
|
||||
$ ./cpint
|
||||
Compiling data for ride on Fri Apr 28 10:48:33 2006...done.
|
||||
Compiling data for ride on Sat Apr 29 10:07:48 2006...done.
|
||||
Compiling data for ride on Sun Apr 30 14:00:17 2006...done.
|
||||
...
|
||||
Compiling data for ride on Mon May 22 18:28:47 2006...done.
|
||||
0.021 1264
|
||||
0.042 1221
|
||||
0.063 1216
|
||||
...
|
||||
5.019 391
|
||||
...
|
||||
171.885 163
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Over this set of rides, the rider's maximum power is 1264 watts, achieved over
|
||||
an interval of 0.021 minutes (1.26 seconds). Over all five-minute
|
||||
subintervals, he has achieved a maximum average power of 391 watts. The
|
||||
longest ride in this set was 171.885 minutes long, and he averaged 163 watts
|
||||
over it.
|
||||
|
||||
<p>
|
||||
We can graph the output of <code>cpint</code> using <code>gnuplot</code> with
|
||||
<a href="cpint.gp">cpint.gp</a>:
|
||||
|
||||
<pre>
|
||||
$ ./cpint > cpint.out
|
||||
$ gnuplot cpint.gp
|
||||
</pre>
|
||||
|
||||
<img src="cpint.png">
|
||||
|
||||
<p>
|
||||
The first time you run <code>cpint</code> it will take a while, as it has to
|
||||
analyze all your past rides. On subsequent runs, however, it will only
|
||||
analyze new files.
|
||||
|
||||
<p><i>Training and Racing with a Power Meter</i> (see the <a
|
||||
href="faq.html">FAQ</a>) contains a table of critical powers of Cat 5 cyclists
|
||||
up through international pros at interval lengths of 5 seconds, 1 minute, 5
|
||||
minutes, and 60 minutes. Using this table and the <code>cpint</code> program,
|
||||
you can determine whether you're stronger than others in your racing category
|
||||
at each interval length and adapt your training program accordingly.
|
||||
|
||||
<p>
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
Converting Old Data
|
||||
Legacy Command-Line Tools
|
||||
</font></big>
|
||||
|
||||
<p>
|
||||
If you've used the PowerTuned software that comes with the PowerTap you may
|
||||
have lots of old ride data in that program that you'd like to include in your
|
||||
critical power graph. You can convert the <code>.xml</code> files that
|
||||
PowerTuned produces to <code>.raw</code> files using the <code>ptpk</code>
|
||||
program:
|
||||
|
||||
<p>
|
||||
<pre>
|
||||
$ ./ptpk 2006_04_27_00_23_28.xml
|
||||
$ head -5 2006_04_27_00_23_28.raw
|
||||
57 56 55 64 02 15
|
||||
60 06 04 7b 80 17
|
||||
40 08 30 00 00 00
|
||||
84 04 00 24 00 ff
|
||||
83 03 00 d7 00 ff
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
<code>ptpk</code> assumes the input <code>.xml</code> file was generated with
|
||||
a wheel size of 2,096 mm and a recording interval of 1. If this is not the
|
||||
case, you should specify the correct values with the <code>-w</code> and
|
||||
<code>-r</code> options.
|
||||
|
||||
<p>
|
||||
Note that the PowerTuned software computes the output speed in miles per hour
|
||||
by multiplying the measured speed in kilometers per hour by 0.62, and the
|
||||
miles per hour values in a <code>.xml</code> file are thus only accurate to
|
||||
two significant figures, even though they're printed out to three decimal
|
||||
places. Because of this limitation, the sequence <code>ptpk</code>,
|
||||
<code>ptunpk</code> is not quite the identity function; in particular, the
|
||||
wattage values from <code>ptpk</code> may only be accurate to two significant
|
||||
digits.
|
||||
You can still build the older, command-line tools from the source code, but we
|
||||
no longer include them in releases. <a href="command-line.html">You can find
|
||||
documentation for them here.</a>
|
||||
|
||||
|
||||
29
doc/wishlist.content
Normal file
@@ -0,0 +1,29 @@
|
||||
<big><font face="arial,helvetica,sanserif">
|
||||
I wish GoldenCheetah would let me...
|
||||
</font></big>
|
||||
<ul>
|
||||
<li>Split one ride into multiple rides</li>
|
||||
<li>Download directly from SRM</li>
|
||||
<li>Graph ride metrics (daily hours, work, BikeScore) over the long
|
||||
term (weeks, seasons)</li>
|
||||
<li>Automatically calculate CP from .cpi files</li>
|
||||
<li>Display the numbers at the bottom of the ride plot, like the
|
||||
critical power graph does</li>
|
||||
<li>Select intervals in the ride plot and display metrics for them
|
||||
at the bottom</li>
|
||||
<li>Pop up an "importing rides" thermometer when importing</li>
|
||||
<li>Remember last settings for showPower, showHr, etc., in ride plot</li>
|
||||
<li>Switch ride plot x-axis from time to distance</li>
|
||||
<li>Edit power zones in a dialog</li>
|
||||
<li>-done- Sort rides in AllRides newest to oldest</li>
|
||||
<li>Group rides list into seasons</li>
|
||||
<li>Group rides list by type, course</li>
|
||||
<li>Add lines to CP plot for seasons, last six (eight?) weeks, etc.</li>
|
||||
<li>Create new intervals</li>
|
||||
<li>Ignore zeros in power histogram</li>
|
||||
<li>Show mulitple rides (seasons, etc.) in power histogram</li>
|
||||
<li>Annotate ride plot</li>
|
||||
<li>Label rides by type, course</li>
|
||||
<li>Use the current date as default when importing CSV files</li>
|
||||
</ul>
|
||||
|
||||
69
doc/zones.content
Normal file
@@ -0,0 +1,69 @@
|
||||
<!-- $Id: download.content,v 1.6 2006/08/11 20:21:03 srhea Exp $ -->
|
||||
|
||||
The zone file format consists of a list of date ranges and the power
|
||||
zones that should be used for all rides within each range. Someday I'll
|
||||
add a dialog to GC that allows you to type in your zones within the
|
||||
application. Right now, you'll have to write them into a text file
|
||||
yourself. For example:
|
||||
|
||||
<blockquote>
|
||||
<pre>
|
||||
# Power zones for Sean Rhea
|
||||
|
||||
From BEGIN until 2006/07/17: # after original testing
|
||||
1, Active Recovery, 122, 167
|
||||
2, Endurance, 167, 228
|
||||
3, Tempo, 228, 274
|
||||
4, Lactate Threshold, 274, 319
|
||||
5, VO2 Max, 319, 365
|
||||
6, Anaerobic Capacity, 365, 678
|
||||
7, Sprinting, 678, MAX
|
||||
|
||||
From 2006/07/17 until 2007/02/05: # since Workingman's ITT
|
||||
1, Active Recovery, 135, 185
|
||||
2, Endurance, 185, 253
|
||||
3, Tempo, 253, 303
|
||||
4, Lactate Threshold, 303, 354
|
||||
5, VO2 Max, 354, 404
|
||||
6, Anaerobic Capacity, 404, 752
|
||||
7, Sprinting, 752, MAX
|
||||
|
||||
From 2007/02/05 until END: # since 20-min Diablo ITT
|
||||
1, Active Recovery, 139, 191
|
||||
2, Endurance, 191, 260
|
||||
3, Tempo, 260, 312
|
||||
4, Lactate Threshold, 312, 364
|
||||
5, VO2 Max, 364, 416
|
||||
6, Anaerobic Capacity, 416, 774
|
||||
7, Sprinting, 774, MAX
|
||||
</pre>
|
||||
</blockquote>
|
||||
|
||||
If you copy the above into a file named "power.zones" in your
|
||||
GoldenCheetah directory (e.g., "~/Library/GoldenCheetah/YourName"), GC
|
||||
will display power zone information for all your rides using my power
|
||||
zones.
|
||||
|
||||
<p>
|
||||
The format should be pretty obvious. Comments start from a '#'
|
||||
character and run until the end of a line.
|
||||
|
||||
<p>
|
||||
A range goes from a date (in YYYY/MM/DD format) at which to start using
|
||||
the following zones to a date before which to stop doing so. Also, you
|
||||
can start the first range in a file with the keyword "BEGIN", which will
|
||||
be treated as the earliest possible date, and you can end the last range
|
||||
with the keyword "END", which will be treated as the latest. Also, in
|
||||
order to have your zones displayed in the "Weekly Summary", each range
|
||||
needs to start on a Monday.
|
||||
|
||||
<p>
|
||||
After a range, you enter the zones to use during that range. Each zone
|
||||
has a name (e.g., "2"), a description (e.g., "Endurance"), a low power
|
||||
and a high power. The number of zones, their names and descriptions are
|
||||
entirely up to you; I use ones similar to those advocated by Allen and
|
||||
Coggan, as you might have noticed. The lower number in each range is
|
||||
inclusive, and the upper one is not; i.e., you should read the range as
|
||||
[low, high). Also, you can use the keywork "MAX" to indicate the
|
||||
maximum possible power.
|
||||
|
||||
2
src/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Added this line to .gitattributes
|
||||
*.pbxproj -crlf -diff -merge
|
||||
27
src/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# xcode noise
|
||||
build/*
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
Info.plist
|
||||
*.xcodeproj
|
||||
gcconfig.pri
|
||||
|
||||
# old skool
|
||||
.svn
|
||||
|
||||
# osx noise
|
||||
.DS_Store
|
||||
profile
|
||||
|
||||
# ignore Qt moc files
|
||||
moc_*
|
||||
qrc_application.cpp
|
||||
|
||||
# ignore other object files
|
||||
*.o
|
||||
|
||||
# ignore qmake-generated Makefile
|
||||
Makefile
|
||||
|
||||
# ignore executables
|
||||
GoldenCheetah*
|
||||
658
src/AllPlot.cpp
Normal file
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "AllPlot.h"
|
||||
#include "RideFile.h"
|
||||
#include "RideItem.h"
|
||||
#include "Settings.h"
|
||||
#include "Zones.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <qwt_plot_curve.h>
|
||||
#include <qwt_plot_grid.h>
|
||||
#include <qwt_plot_marker.h>
|
||||
#include <qwt_text.h>
|
||||
#include <qwt_legend.h>
|
||||
#include <qwt_data.h>
|
||||
#include <QMultiMap>
|
||||
|
||||
|
||||
// define a background class to handle shading of power zones
|
||||
// draws power zone bands IF zones are defined and the option
|
||||
// to draw bonds has been selected
|
||||
class AllPlotBackground: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
AllPlot *parent;
|
||||
|
||||
public:
|
||||
AllPlotBackground(AllPlot *_parent)
|
||||
{
|
||||
setZ(0.0);
|
||||
parent = _parent;
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
virtual void draw(QPainter *painter,
|
||||
const QwtScaleMap &, const QwtScaleMap &yMap,
|
||||
const QRect &rect) const
|
||||
{
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
if (num_zones > 0) {
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
QRect r = rect;
|
||||
|
||||
QColor shading_color = zoneColor(z, num_zones);
|
||||
shading_color.setHsv(
|
||||
shading_color.hue(),
|
||||
shading_color.saturation() / 4,
|
||||
shading_color.value()
|
||||
);
|
||||
r.setBottom(yMap.transform(zone_lows[z]));
|
||||
if (z + 1 < num_zones)
|
||||
r.setTop(yMap.transform(zone_lows[z + 1]));
|
||||
if (r.top() <= r.bottom())
|
||||
painter->fillRect(r, shading_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Zone labels are drawn if power zone bands are enabled, automatically
|
||||
// at the center of the plot
|
||||
class AllPlotZoneLabel: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
AllPlot *parent;
|
||||
int zone_number;
|
||||
double watts;
|
||||
QwtText text;
|
||||
|
||||
public:
|
||||
AllPlotZoneLabel(AllPlot *_parent, int _zone_number)
|
||||
{
|
||||
parent = _parent;
|
||||
zone_number = _zone_number;
|
||||
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
// create new zone labels if we're shading
|
||||
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
|
||||
QList <QString> zone_names = (*zones)->getZoneNames(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
assert(zone_names.size() == num_zones);
|
||||
if (zone_number < num_zones) {
|
||||
watts =
|
||||
(
|
||||
(zone_number + 1 < num_zones) ?
|
||||
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
|
||||
(
|
||||
(zone_number > 0) ?
|
||||
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
|
||||
2.0 * zone_lows[zone_number]
|
||||
)
|
||||
);
|
||||
|
||||
text = QwtText(zone_names[zone_number]);
|
||||
text.setFont(QFont("Helvetica",24, QFont::Bold));
|
||||
QColor text_color = zoneColor(zone_number, num_zones);
|
||||
text_color.setAlpha(64);
|
||||
text.setColor(text_color);
|
||||
}
|
||||
}
|
||||
|
||||
setZ(1.0 + zone_number / 100.0);
|
||||
}
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
void draw(QPainter *painter,
|
||||
const QwtScaleMap &, const QwtScaleMap &yMap,
|
||||
const QRect &rect) const
|
||||
{
|
||||
if (parent->shadeZones()) {
|
||||
int x = (rect.left() + rect.right()) / 2;
|
||||
int y = yMap.transform(watts);
|
||||
|
||||
// the following code based on source for QwtPlotMarker::draw()
|
||||
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
|
||||
tr.moveCenter(QPoint(x, y));
|
||||
text.draw(painter, tr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
static inline double
|
||||
max(double a, double b) { if (a > b) return a; else return b; }
|
||||
|
||||
#define MILES_PER_KM 0.62137119
|
||||
#define FEET_PER_M 3.2808399
|
||||
|
||||
AllPlot::AllPlot():
|
||||
settings(NULL),
|
||||
unit(0),
|
||||
d_mrk(NULL), rideItem(NULL),
|
||||
hrArray(NULL), wattsArray(NULL),
|
||||
speedArray(NULL), cadArray(NULL), timeArray(NULL),
|
||||
distanceArray(NULL), altArray(NULL), interArray(NULL), smooth(30), bydist(false),
|
||||
shade_zones(false)
|
||||
{
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
unit = settings->value(GC_UNIT);
|
||||
|
||||
useMetricUnits = (unit.toString() == "Metric");
|
||||
|
||||
// create a background object for shading
|
||||
bg = new AllPlotBackground(this);
|
||||
bg->attach(this);
|
||||
|
||||
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
|
||||
setCanvasBackground(Qt::white);
|
||||
|
||||
setXTitle();
|
||||
|
||||
wattsCurve = new QwtPlotCurve("Power");
|
||||
QPen wattsPen = QPen(Qt::red);
|
||||
wattsPen.setWidth(2);
|
||||
wattsCurve->setPen(wattsPen);
|
||||
|
||||
hrCurve = new QwtPlotCurve("Heart Rate");
|
||||
QPen hrPen = QPen(Qt::blue);
|
||||
hrPen.setWidth(2);
|
||||
hrCurve->setPen(hrPen);
|
||||
|
||||
speedCurve = new QwtPlotCurve("Speed");
|
||||
QPen speedPen = QPen(QColor(0, 204, 0));
|
||||
speedPen.setWidth(2);
|
||||
speedCurve->setPen(speedPen);
|
||||
speedCurve->setYAxis(yRight);
|
||||
|
||||
cadCurve = new QwtPlotCurve("Cadence");
|
||||
QPen cadPen = QPen(QColor(0, 204, 204));
|
||||
cadPen.setWidth(2);
|
||||
cadCurve->setPen(cadPen);
|
||||
|
||||
altCurve = new QwtPlotCurve("Altitude");
|
||||
// altCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
QPen *altPen = new QPen(QColor(124, 91, 31));
|
||||
altPen->setWidth(1);
|
||||
altCurve->setPen(*altPen);
|
||||
QColor brush_color = QColor(124, 91, 31);
|
||||
brush_color.setAlpha(64);
|
||||
altCurve->setBrush(brush_color); // fill below the line
|
||||
delete altPen;
|
||||
|
||||
grid = new QwtPlotGrid();
|
||||
grid->enableX(false);
|
||||
QPen gridPen;
|
||||
gridPen.setStyle(Qt::DotLine);
|
||||
grid->setPen(gridPen);
|
||||
grid->attach(this);
|
||||
|
||||
zoneLabels = QList <AllPlotZoneLabel *>::QList();
|
||||
}
|
||||
|
||||
struct DataPoint {
|
||||
double time, hr, watts, speed, cad, alt;
|
||||
int inter;
|
||||
DataPoint(double t, double h, double w, double s, double c, double a, int i) :
|
||||
time(t), hr(h), watts(w), speed(s), cad(c), alt(a), inter(i) {}
|
||||
};
|
||||
|
||||
bool AllPlot::shadeZones() const
|
||||
{
|
||||
return (shade_zones && wattsArray);
|
||||
}
|
||||
|
||||
void AllPlot::refreshZoneLabels()
|
||||
{
|
||||
// delete any existing power zone labels
|
||||
if (zoneLabels.size()) {
|
||||
QListIterator<AllPlotZoneLabel *> i(zoneLabels);
|
||||
while (i.hasNext()) {
|
||||
AllPlotZoneLabel *label = i.next();
|
||||
label->detach();
|
||||
delete label;
|
||||
}
|
||||
}
|
||||
|
||||
zoneLabels.clear();
|
||||
|
||||
if (rideItem) {
|
||||
int zone_range = rideItem->zoneRange();
|
||||
Zones **zones = rideItem->zones;
|
||||
|
||||
// generate labels for existing zones
|
||||
if (zones && *zones && (zone_range >= 0)) {
|
||||
int num_zones = (*zones)->numZones(zone_range);
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
AllPlotZoneLabel *label = new AllPlotZoneLabel(this, z);
|
||||
label->attach(this);
|
||||
zoneLabels.append(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
AllPlot::recalc()
|
||||
{
|
||||
if (!timeArray)
|
||||
return;
|
||||
int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]);
|
||||
if (rideTimeSecs > 7*24*60*60) {
|
||||
QwtArray<double> data;
|
||||
if (wattsArray)
|
||||
wattsCurve->setData(data, data);
|
||||
if (hrArray)
|
||||
hrCurve->setData(data, data);
|
||||
if (speedArray)
|
||||
speedCurve->setData(data, data);
|
||||
if (cadArray)
|
||||
cadCurve->setData(data, data);
|
||||
if (altArray)
|
||||
altCurve->setData(data, data);
|
||||
return;
|
||||
}
|
||||
double totalWatts = 0.0;
|
||||
double totalHr = 0.0;
|
||||
double totalSpeed = 0.0;
|
||||
double totalCad = 0.0;
|
||||
double totalDist = 0.0;
|
||||
double totalAlt = 0.0;
|
||||
|
||||
QList<DataPoint*> list;
|
||||
|
||||
double *smoothWatts = new double[rideTimeSecs + 1];
|
||||
double *smoothHr = new double[rideTimeSecs + 1];
|
||||
double *smoothSpeed = new double[rideTimeSecs + 1];
|
||||
double *smoothCad = new double[rideTimeSecs + 1];
|
||||
double *smoothTime = new double[rideTimeSecs + 1];
|
||||
double *smoothDistance = new double[rideTimeSecs + 1];
|
||||
double *smoothAltitude = new double[rideTimeSecs + 1];
|
||||
|
||||
delete [] d_mrk;
|
||||
QMap<double, int> interList; //Store the times and intervals
|
||||
// Times are unique, intervals are not always
|
||||
//Intervals are sequential on the PowerTap.
|
||||
|
||||
int lastInterval = 0; //Detect if we hit a new interval
|
||||
|
||||
for (int secs = 0; ((secs < smooth)
|
||||
&& (secs < rideTimeSecs)); ++secs) {
|
||||
smoothWatts[secs] = 0.0;
|
||||
smoothHr[secs] = 0.0;
|
||||
smoothSpeed[secs] = 0.0;
|
||||
smoothCad[secs] = 0.0;
|
||||
smoothTime[secs] = secs / 60.0;
|
||||
smoothDistance[secs] = 0.0;
|
||||
smoothAltitude[secs] = 0.0;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (int secs = smooth; secs <= rideTimeSecs; ++secs) {
|
||||
while ((i < arrayLength) && (timeArray[i] <= secs)) {
|
||||
DataPoint *dp =
|
||||
new DataPoint(
|
||||
timeArray[i],
|
||||
(hrArray ? hrArray[i] : 0),
|
||||
(wattsArray ? wattsArray[i] : 0),
|
||||
(speedArray ? speedArray[i] : 0),
|
||||
(cadArray ? cadArray[i] : 0),
|
||||
(altArray ? altArray[i] : 0),
|
||||
interArray[i]
|
||||
);
|
||||
if (wattsArray)
|
||||
totalWatts += wattsArray[i];
|
||||
if (hrArray)
|
||||
totalHr += hrArray[i];
|
||||
if (speedArray)
|
||||
totalSpeed += speedArray[i];
|
||||
if (cadArray)
|
||||
totalCad += cadArray[i];
|
||||
if (altArray)
|
||||
totalAlt += altArray[i];
|
||||
totalDist = distanceArray[i];
|
||||
list.append(dp);
|
||||
//Figure out when and if we have a new interval..
|
||||
if(lastInterval != interArray[i]) {
|
||||
lastInterval = interArray[i];
|
||||
interList[secs/60.0] = lastInterval;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
while (!list.empty() && (list.front()->time < secs - smooth)) {
|
||||
DataPoint *dp = list.front();
|
||||
list.removeFirst();
|
||||
totalWatts -= dp->watts;
|
||||
totalHr -= dp->hr;
|
||||
totalSpeed -= dp->speed;
|
||||
totalCad -= dp->cad;
|
||||
totalAlt -= dp->alt;
|
||||
delete dp;
|
||||
}
|
||||
// TODO: this is wrong. We should do a weighted average over the
|
||||
// seconds represented by each point...
|
||||
if (list.empty()) {
|
||||
smoothWatts[secs] = 0.0;
|
||||
smoothHr[secs] = 0.0;
|
||||
smoothSpeed[secs] = 0.0;
|
||||
smoothCad[secs] = 0.0;
|
||||
smoothAltitude[secs] = smoothAltitude[secs - 1];
|
||||
}
|
||||
else {
|
||||
smoothWatts[secs] = totalWatts / list.size();
|
||||
smoothHr[secs] = totalHr / list.size();
|
||||
smoothSpeed[secs] = totalSpeed / list.size();
|
||||
smoothCad[secs] = totalCad / list.size();
|
||||
smoothAltitude[secs] = totalAlt / list.size();
|
||||
}
|
||||
smoothDistance[secs] = totalDist;
|
||||
smoothTime[secs] = secs / 60.0;
|
||||
}
|
||||
|
||||
double *xaxis = bydist ? smoothDistance : smoothTime;
|
||||
|
||||
// set curves
|
||||
if (wattsArray)
|
||||
wattsCurve->setData(xaxis, smoothWatts, rideTimeSecs + 1);
|
||||
if (hrArray)
|
||||
hrCurve->setData(xaxis, smoothHr, rideTimeSecs + 1);
|
||||
if (speedArray)
|
||||
speedCurve->setData(xaxis, smoothSpeed, rideTimeSecs + 1);
|
||||
if (cadArray)
|
||||
cadCurve->setData(xaxis, smoothCad, rideTimeSecs + 1);
|
||||
if (altArray)
|
||||
altCurve->setData(xaxis, smoothAltitude, rideTimeSecs + 1);
|
||||
|
||||
setAxisScale(xBottom, 0.0, bydist ? totalDist : smoothTime[rideTimeSecs]);
|
||||
setYMax();
|
||||
|
||||
refreshZoneLabels();
|
||||
|
||||
//QList<double> interTimes = interList.keys();
|
||||
QString label[interList.count()];
|
||||
QwtText text[interList.count()];
|
||||
d_mrk = new QwtPlotMarker[interList.count()];
|
||||
|
||||
int x = 0;
|
||||
double time;
|
||||
foreach(time, interList.keys()) {
|
||||
// marker
|
||||
d_mrk[x].setValue(0,0);
|
||||
d_mrk[x].setLineStyle(QwtPlotMarker::VLine);
|
||||
d_mrk[x].setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
|
||||
d_mrk[x].setLinePen(QPen(Qt::black, 0, Qt::DashDotLine));
|
||||
d_mrk[x].attach(this);
|
||||
label[x] = QString::number(interList[time]);
|
||||
text[x] = QwtText(label[x]);
|
||||
text[x].setFont(QFont("Helvetica", 10, QFont::Bold));
|
||||
text[x].setColor(Qt::black);
|
||||
if (!bydist)
|
||||
d_mrk[x].setValue(time, 0.0);
|
||||
else
|
||||
d_mrk[x].setValue(smoothDistance[int(ceil(60*time))], 0.0);
|
||||
d_mrk[x].setLabel(text[x]);
|
||||
x++;
|
||||
}
|
||||
|
||||
replot();
|
||||
|
||||
if(smoothWatts != NULL)
|
||||
delete [] smoothWatts;
|
||||
if(smoothHr != NULL)
|
||||
delete [] smoothHr;
|
||||
if(smoothSpeed != NULL)
|
||||
delete [] smoothSpeed;
|
||||
if(smoothCad != NULL)
|
||||
delete [] smoothCad;
|
||||
if(smoothTime != NULL)
|
||||
delete [] smoothTime;
|
||||
if(smoothDistance != NULL)
|
||||
delete [] smoothDistance;
|
||||
if(smoothAltitude != NULL)
|
||||
delete [] smoothAltitude;
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::setYMax()
|
||||
{
|
||||
double ymax = 0;
|
||||
QString ylabel = "";
|
||||
if (wattsCurve->isVisible()) {
|
||||
ymax = max(ymax, wattsCurve->maxYValue());
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + "Watts";
|
||||
}
|
||||
if (hrCurve->isVisible()) {
|
||||
ymax = max(ymax, hrCurve->maxYValue());
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + "BPM";
|
||||
}
|
||||
if (cadCurve->isVisible()) {
|
||||
ymax = max(ymax, cadCurve->maxYValue());
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + "RPM";
|
||||
}
|
||||
if (altCurve->isVisible()) {
|
||||
ymax = max(ymax, altCurve->maxYValue());
|
||||
if (useMetricUnits){
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + "Meters";
|
||||
} else {
|
||||
ylabel += QString((ylabel == "") ? "" : " / ") + "Ft";
|
||||
}
|
||||
}
|
||||
|
||||
setAxisScale(yLeft, 0.0, ymax * 1.1);
|
||||
setAxisTitle(yLeft, ylabel);
|
||||
|
||||
enableAxis(yRight, speedCurve->isVisible());
|
||||
setAxisTitle(yRight, (useMetricUnits ? "KPH" : "MPH"));
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::setXTitle()
|
||||
{
|
||||
if (bydist)
|
||||
setAxisTitle(xBottom, "Distance "+QString(unit.toString() == "Metric"?"(km)":"(miles)"));
|
||||
else
|
||||
setAxisTitle(xBottom, "Time (minutes)");
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::setData(RideItem *_rideItem)
|
||||
{
|
||||
rideItem = _rideItem;
|
||||
|
||||
if(wattsArray != NULL)
|
||||
delete [] wattsArray;
|
||||
if(hrArray != NULL)
|
||||
delete [] hrArray;
|
||||
if(speedArray != NULL)
|
||||
delete [] speedArray;
|
||||
if(cadArray != NULL)
|
||||
delete [] cadArray;
|
||||
if(timeArray != NULL)
|
||||
delete [] timeArray;
|
||||
if(interArray != NULL)
|
||||
delete [] interArray;
|
||||
if(distanceArray != NULL)
|
||||
delete [] distanceArray;
|
||||
if(altArray != NULL)
|
||||
delete [] altArray;
|
||||
|
||||
RideFile *ride = rideItem->ride;
|
||||
if (ride) {
|
||||
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
|
||||
|
||||
RideFileDataPresent *dataPresent = ride->areDataPresent();
|
||||
int npoints = ride->dataPoints().size();
|
||||
wattsArray = dataPresent->watts ? new double[npoints] : NULL;
|
||||
hrArray = dataPresent->hr ? new double[npoints] : NULL;
|
||||
speedArray = dataPresent->kph ? new double[npoints] : NULL;
|
||||
cadArray = dataPresent->cad ? new double[npoints] : NULL;
|
||||
altArray = dataPresent->alt ? new double[npoints] : NULL;
|
||||
timeArray = new double[npoints];
|
||||
interArray = new int[npoints];
|
||||
distanceArray = new double[npoints];
|
||||
|
||||
// attach appropriate curves
|
||||
wattsCurve->detach();
|
||||
hrCurve->detach();
|
||||
speedCurve->detach();
|
||||
cadCurve->detach();
|
||||
altCurve->detach();
|
||||
if (wattsArray) wattsCurve->attach(this);
|
||||
if (hrArray) hrCurve->attach(this);
|
||||
if (speedArray) speedCurve->attach(this);
|
||||
if (cadArray) cadCurve->attach(this);
|
||||
if (altArray) altCurve->attach(this);
|
||||
|
||||
arrayLength = 0;
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
while (i.hasNext()) {
|
||||
RideFilePoint *point = i.next();
|
||||
timeArray[arrayLength] = point->secs;
|
||||
if (wattsArray)
|
||||
wattsArray[arrayLength] = max(0, point->watts);
|
||||
if (hrArray)
|
||||
hrArray[arrayLength] = max(0, point->hr);
|
||||
if (speedArray)
|
||||
speedArray[arrayLength] = max(0,
|
||||
(useMetricUnits
|
||||
? point->kph
|
||||
: point->kph * MILES_PER_KM));
|
||||
if (cadArray)
|
||||
cadArray[arrayLength] = max(0, point->cad);
|
||||
if (altArray)
|
||||
altArray[arrayLength] = max(0,
|
||||
(useMetricUnits
|
||||
? point->alt
|
||||
: point->alt * FEET_PER_M));
|
||||
interArray[arrayLength] = point->interval;
|
||||
distanceArray[arrayLength] = max(0,
|
||||
(useMetricUnits
|
||||
? point->km
|
||||
: point->km * MILES_PER_KM));
|
||||
++arrayLength;
|
||||
}
|
||||
|
||||
recalc();
|
||||
}
|
||||
else {
|
||||
setTitle("no data");
|
||||
wattsCurve->detach();
|
||||
hrCurve->detach();
|
||||
speedCurve->detach();
|
||||
cadCurve->detach();
|
||||
altCurve->detach();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::showPower(int id)
|
||||
{
|
||||
wattsCurve->setVisible(id < 2);
|
||||
shade_zones = (id == 0);
|
||||
setYMax();
|
||||
recalc();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::showHr(int state)
|
||||
{
|
||||
assert(state != Qt::PartiallyChecked);
|
||||
hrCurve->setVisible(state == Qt::Checked);
|
||||
setYMax();
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::showSpeed(int state)
|
||||
{
|
||||
assert(state != Qt::PartiallyChecked);
|
||||
speedCurve->setVisible(state == Qt::Checked);
|
||||
setYMax();
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::showCad(int state)
|
||||
{
|
||||
assert(state != Qt::PartiallyChecked);
|
||||
cadCurve->setVisible(state == Qt::Checked);
|
||||
setYMax();
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::showAlt(int state)
|
||||
{
|
||||
assert(state != Qt::PartiallyChecked);
|
||||
altCurve->setVisible(state == Qt::Checked);
|
||||
setYMax();
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::showGrid(int state)
|
||||
{
|
||||
assert(state != Qt::PartiallyChecked);
|
||||
grid->setVisible(state == Qt::Checked);
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::setSmoothing(int value)
|
||||
{
|
||||
smooth = value;
|
||||
recalc();
|
||||
}
|
||||
|
||||
void
|
||||
AllPlot::setByDistance(int id)
|
||||
{
|
||||
bydist = (id == 1);
|
||||
setXTitle();
|
||||
recalc();
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: AllPlot.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
@@ -22,27 +20,51 @@
|
||||
#define _GC_AllPlot_h 1
|
||||
|
||||
#include <qwt_plot.h>
|
||||
#include <QtGui>
|
||||
#include <qpainter.h>
|
||||
|
||||
class QPen;
|
||||
class QwtPlotCurve;
|
||||
class QwtPlotGrid;
|
||||
class RawFile;
|
||||
class QwtPlotMarker;
|
||||
class RideItem;
|
||||
class RideFile;
|
||||
class AllPlot;
|
||||
class AllPlotBackground;
|
||||
class AllPlotZoneLabel;
|
||||
|
||||
class AllPlot : public QwtPlot
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
|
||||
AllPlotBackground *bg;
|
||||
QSettings *settings;
|
||||
QVariant unit;
|
||||
|
||||
public:
|
||||
|
||||
QwtPlotCurve *wattsCurve;
|
||||
QwtPlotCurve *hrCurve;
|
||||
QwtPlotCurve *speedCurve;
|
||||
QwtPlotCurve *cadCurve;
|
||||
QwtPlotCurve *altCurve;
|
||||
QwtPlotMarker *d_mrk;
|
||||
QList <AllPlotZoneLabel *> zoneLabels;
|
||||
|
||||
AllPlot();
|
||||
|
||||
int smoothing() const { return smooth; }
|
||||
|
||||
void setData(RawFile *raw);
|
||||
bool byDistance() const { return bydist; }
|
||||
|
||||
bool shadeZones() const;
|
||||
void refreshZoneLabels();
|
||||
|
||||
void setData(RideItem *_rideItem);
|
||||
|
||||
RideItem *rideItem;
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -50,8 +72,10 @@ class AllPlot : public QwtPlot
|
||||
void showHr(int state);
|
||||
void showSpeed(int state);
|
||||
void showCad(int state);
|
||||
void showAlt(int state);
|
||||
void showGrid(int state);
|
||||
void setSmoothing(int value);
|
||||
void setByDistance(int value);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -62,12 +86,21 @@ class AllPlot : public QwtPlot
|
||||
double *speedArray;
|
||||
double *cadArray;
|
||||
double *timeArray;
|
||||
double *distanceArray;
|
||||
double *altArray;
|
||||
int arrayLength;
|
||||
int *interArray;
|
||||
|
||||
int smooth;
|
||||
|
||||
bool bydist;
|
||||
|
||||
void recalc();
|
||||
void setYMax();
|
||||
void setXTitle();
|
||||
|
||||
bool shade_zones; // whether power should be shaded
|
||||
bool useMetricUnits; // whether metric units are used (or imperial)
|
||||
};
|
||||
|
||||
#endif // _GC_AllPlot_h
|
||||
256
src/BasicRideMetrics.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideMetric.h"
|
||||
|
||||
#define MILES_PER_KM 0.62137119
|
||||
#define FEET_PER_METER 3.2808399
|
||||
|
||||
class WorkoutTime : public RideMetric {
|
||||
double seconds;
|
||||
|
||||
public:
|
||||
|
||||
WorkoutTime() : seconds(0.0) {}
|
||||
QString name() const { return "workout_time"; }
|
||||
QString units(bool) const { return "seconds"; }
|
||||
double value(bool) const { return seconds; }
|
||||
void compute(const RideFile *ride, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
seconds = ride->dataPoints().back()->secs;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) { seconds += other->value(true); }
|
||||
RideMetric *clone() const { return new WorkoutTime(*this); }
|
||||
};
|
||||
|
||||
static bool workoutTimeAdded =
|
||||
RideMetricFactory::instance().addMetric(WorkoutTime());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class TimeRiding : public PointwiseRideMetric {
|
||||
double secsMovingOrPedaling;
|
||||
|
||||
public:
|
||||
|
||||
TimeRiding() : secsMovingOrPedaling(0.0) {}
|
||||
QString name() const { return "time_riding"; }
|
||||
QString units(bool) const { return "seconds"; }
|
||||
double value(bool) const { return secsMovingOrPedaling; }
|
||||
void perPoint(const RideFilePoint *point, double secsDelta,
|
||||
const RideFile *, const Zones *, int) {
|
||||
if ((point->kph > 0.0) || (point->cad > 0.0))
|
||||
secsMovingOrPedaling += secsDelta;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
secsMovingOrPedaling += other->value(true);
|
||||
}
|
||||
RideMetric *clone() const { return new TimeRiding(*this); }
|
||||
};
|
||||
|
||||
static bool timeRidingAdded =
|
||||
RideMetricFactory::instance().addMetric(TimeRiding());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class TotalDistance : public RideMetric {
|
||||
double km;
|
||||
|
||||
public:
|
||||
|
||||
TotalDistance() : km(0.0) {}
|
||||
QString name() const { return "total_distance"; }
|
||||
QString units(bool metric) const { return metric ? "km" : "miles"; }
|
||||
double value(bool metric) const {
|
||||
return metric ? km : (km * MILES_PER_KM);
|
||||
}
|
||||
void compute(const RideFile *ride, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
km = ride->dataPoints().back()->km;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) { km += other->value(true); }
|
||||
RideMetric *clone() const { return new TotalDistance(*this); }
|
||||
};
|
||||
|
||||
static bool totalDistanceAdded =
|
||||
RideMetricFactory::instance().addMetric(TotalDistance());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
class ElevationGain : public PointwiseRideMetric {
|
||||
double elegain;
|
||||
double prevalt;
|
||||
|
||||
public:
|
||||
|
||||
ElevationGain() : elegain(0.0), prevalt(0.0) {}
|
||||
QString name() const { return "elevation_gain"; }
|
||||
QString units(bool metric) const { return metric ? "meters" : "feet"; }
|
||||
double value(bool metric) const {
|
||||
return metric ? elegain : (elegain * FEET_PER_METER);
|
||||
}
|
||||
void perPoint(const RideFilePoint *point, double,
|
||||
const RideFile *, const Zones *, int) {
|
||||
|
||||
if (prevalt <= 0){
|
||||
prevalt = point->alt;
|
||||
} else if (prevalt <= point->alt) {
|
||||
elegain += (point->alt-prevalt);
|
||||
prevalt = point->alt;
|
||||
} else {
|
||||
prevalt = point->alt;
|
||||
}
|
||||
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
elegain += other->value(true);
|
||||
}
|
||||
RideMetric *clone() const { return new ElevationGain(*this); }
|
||||
};
|
||||
|
||||
static bool elevationGainAdded =
|
||||
RideMetricFactory::instance().addMetric(ElevationGain());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class TotalWork : public PointwiseRideMetric {
|
||||
double joules;
|
||||
|
||||
public:
|
||||
|
||||
TotalWork() : joules(0.0) {}
|
||||
QString name() const { return "total_work"; }
|
||||
QString units(bool) const { return "kJ"; }
|
||||
double value(bool) const { return joules / 1000.0; }
|
||||
void perPoint(const RideFilePoint *point, double secsDelta,
|
||||
const RideFile *, const Zones *, int) {
|
||||
if (point->watts >= 0.0)
|
||||
joules += point->watts * secsDelta;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
TotalWork *tw = dynamic_cast<TotalWork*>(other);
|
||||
joules += tw->joules;
|
||||
}
|
||||
RideMetric *clone() const { return new TotalWork(*this); }
|
||||
};
|
||||
|
||||
static bool totalWorkAdded =
|
||||
RideMetricFactory::instance().addMetric(TotalWork());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class AvgSpeed : public PointwiseRideMetric {
|
||||
double secsMoving;
|
||||
double km;
|
||||
|
||||
public:
|
||||
|
||||
AvgSpeed() : secsMoving(0.0), km(0.0) {}
|
||||
QString name() const { return "average_speed"; }
|
||||
QString units(bool metric) const { return metric ? "kph" : "mph"; }
|
||||
double value(bool metric) const {
|
||||
if (secsMoving == 0.0) return 0.0;
|
||||
double kph = km / secsMoving * 3600.0;
|
||||
return metric ? kph : (kph * MILES_PER_KM);
|
||||
}
|
||||
void compute(const RideFile *ride, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) {
|
||||
PointwiseRideMetric::compute(ride, zones, zoneRange, deps);
|
||||
km = ride->dataPoints().back()->km;
|
||||
}
|
||||
void perPoint(const RideFilePoint *point, double secsDelta,
|
||||
const RideFile *, const Zones *, int) {
|
||||
if (point->kph > 0.0) secsMoving += secsDelta;
|
||||
}
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
AvgSpeed *as = dynamic_cast<AvgSpeed*>(other);
|
||||
secsMoving += as->secsMoving;
|
||||
km += as->km;
|
||||
}
|
||||
RideMetric *clone() const { return new AvgSpeed(*this); }
|
||||
};
|
||||
|
||||
static bool avgSpeedAdded =
|
||||
RideMetricFactory::instance().addMetric(AvgSpeed());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct AvgPower : public AvgRideMetric {
|
||||
|
||||
QString name() const { return "average_power"; }
|
||||
QString units(bool) const { return "watts"; }
|
||||
void perPoint(const RideFilePoint *point, double,
|
||||
const RideFile *, const Zones *, int) {
|
||||
if (point->watts >= 0.0) {
|
||||
total += point->watts;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
RideMetric *clone() const { return new AvgPower(*this); }
|
||||
};
|
||||
|
||||
static bool avgPowerAdded =
|
||||
RideMetricFactory::instance().addMetric(AvgPower());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct AvgHeartRate : public AvgRideMetric {
|
||||
|
||||
QString name() const { return "average_hr"; }
|
||||
QString units(bool) const { return "bpm"; }
|
||||
void perPoint(const RideFilePoint *point, double,
|
||||
const RideFile *, const Zones *, int) {
|
||||
if (point->hr > 0) {
|
||||
total += point->hr;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
RideMetric *clone() const { return new AvgHeartRate(*this); }
|
||||
};
|
||||
|
||||
static bool avgHeartRateAdded =
|
||||
RideMetricFactory::instance().addMetric(AvgHeartRate());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct AvgCadence : public AvgRideMetric {
|
||||
|
||||
QString name() const { return "average_cad"; }
|
||||
QString units(bool) const { return "rpm"; }
|
||||
void perPoint(const RideFilePoint *point, double,
|
||||
const RideFile *, const Zones *, int) {
|
||||
if (point->cad > 0) {
|
||||
total += point->cad;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
RideMetric *clone() const { return new AvgCadence(*this); }
|
||||
};
|
||||
|
||||
static bool avgCadenceAdded =
|
||||
RideMetricFactory::instance().addMetric(AvgCadence());
|
||||
|
||||
179
src/BestIntervalDialog.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "BestIntervalDialog.h"
|
||||
#include "MainWindow.h"
|
||||
#include "RideFile.h"
|
||||
#include <QMap>
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
|
||||
BestIntervalDialog::BestIntervalDialog(MainWindow *mainWindow) :
|
||||
mainWindow(mainWindow)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle("Find Best Intervals");
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
QHBoxLayout *intervalLengthLayout = new QHBoxLayout;
|
||||
QLabel *intervalLengthLabel = new QLabel(tr("Interval length: "), this);
|
||||
intervalLengthLayout->addWidget(intervalLengthLabel);
|
||||
hrsSpinBox = new QDoubleSpinBox(this);
|
||||
hrsSpinBox->setDecimals(0);
|
||||
hrsSpinBox->setMinimum(0.0);
|
||||
hrsSpinBox->setSuffix(" hrs");
|
||||
hrsSpinBox->setSingleStep(1.0);
|
||||
hrsSpinBox->setAlignment(Qt::AlignRight);
|
||||
intervalLengthLayout->addWidget(hrsSpinBox);
|
||||
minsSpinBox = new QDoubleSpinBox(this);
|
||||
minsSpinBox->setDecimals(0);
|
||||
minsSpinBox->setRange(0.0, 59.0);
|
||||
minsSpinBox->setSuffix(" mins");
|
||||
minsSpinBox->setSingleStep(1.0);
|
||||
minsSpinBox->setWrapping(true);
|
||||
minsSpinBox->setAlignment(Qt::AlignRight);
|
||||
minsSpinBox->setValue(1.0);
|
||||
intervalLengthLayout->addWidget(minsSpinBox);
|
||||
secsSpinBox = new QDoubleSpinBox(this);
|
||||
secsSpinBox->setDecimals(0);
|
||||
minsSpinBox->setRange(0.0, 59.0);
|
||||
secsSpinBox->setSuffix(" secs");
|
||||
secsSpinBox->setSingleStep(1.0);
|
||||
secsSpinBox->setWrapping(true);
|
||||
secsSpinBox->setAlignment(Qt::AlignRight);
|
||||
intervalLengthLayout->addWidget(secsSpinBox);
|
||||
mainLayout->addLayout(intervalLengthLayout);
|
||||
|
||||
QHBoxLayout *intervalCountLayout = new QHBoxLayout;
|
||||
QLabel *intervalCountLabel = new QLabel(tr("How many to find: "), this);
|
||||
intervalCountLayout->addWidget(intervalCountLabel);
|
||||
countSpinBox = new QDoubleSpinBox(this);
|
||||
countSpinBox->setDecimals(0);
|
||||
countSpinBox->setMinimum(1.0);
|
||||
countSpinBox->setSingleStep(1.0);
|
||||
countSpinBox->setAlignment(Qt::AlignRight);
|
||||
intervalCountLayout->addWidget(countSpinBox);
|
||||
mainLayout->addLayout(intervalCountLayout);
|
||||
|
||||
QLabel *resultsLabel = new QLabel(tr("Results:"), this);
|
||||
mainLayout->addWidget(resultsLabel);
|
||||
|
||||
resultsText = new QTextEdit(this);
|
||||
resultsText->setReadOnly(true);
|
||||
mainLayout->addWidget(resultsText);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
findButton = new QPushButton(tr("&Find Intervals"), this);
|
||||
buttonLayout->addWidget(findButton);
|
||||
doneButton = new QPushButton(tr("&Done"), this);
|
||||
buttonLayout->addWidget(doneButton);
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked()));
|
||||
connect(doneButton, SIGNAL(clicked()), this, SLOT(doneClicked()));
|
||||
}
|
||||
|
||||
void
|
||||
BestIntervalDialog::findClicked()
|
||||
{
|
||||
const RideFile *ride = mainWindow->currentRide();
|
||||
if (!ride) {
|
||||
QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!"));
|
||||
return;
|
||||
}
|
||||
|
||||
int maxIntervals = (int) countSpinBox->value();
|
||||
double windowSizeSecs = (hrsSpinBox->value() * 3600.0
|
||||
+ minsSpinBox->value() * 60.0
|
||||
+ secsSpinBox->value());
|
||||
|
||||
if (windowSizeSecs == 0.0) {
|
||||
QMessageBox::critical(this, tr("Bad Interval Length"),
|
||||
tr("Interval length must be greater than zero!"));
|
||||
return;
|
||||
}
|
||||
|
||||
QList<const RideFilePoint*> window;
|
||||
QMap<double,double> bests;
|
||||
|
||||
double secsDelta = ride->recIntSecs();
|
||||
int expectedSamples = (int) floor(windowSizeSecs / secsDelta);
|
||||
double totalWatts = 0.0;
|
||||
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
while (i.hasNext()) {
|
||||
const RideFilePoint *point = i.next();
|
||||
while (!window.empty()
|
||||
&& (point->secs >= window.first()->secs + windowSizeSecs)) {
|
||||
totalWatts -= window.first()->watts;
|
||||
window.takeFirst();
|
||||
}
|
||||
totalWatts += point->watts;
|
||||
window.append(point);
|
||||
int divisor = std::max(window.size(), expectedSamples);
|
||||
double avg = totalWatts / divisor;
|
||||
bests.insertMulti(avg, point->secs);
|
||||
}
|
||||
|
||||
QMap<double,double> results;
|
||||
while (!bests.empty() && maxIntervals--) {
|
||||
QMutableMapIterator<double,double> j(bests);
|
||||
j.toBack();
|
||||
j.previous();
|
||||
double secs = j.value();
|
||||
results.insert(j.value() - windowSizeSecs, j.key());
|
||||
j.remove();
|
||||
while (j.hasPrevious()) {
|
||||
j.previous();
|
||||
if (abs(secs - j.value()) < windowSizeSecs)
|
||||
j.remove();
|
||||
}
|
||||
}
|
||||
|
||||
QString resultsHtml =
|
||||
"<center>"
|
||||
"<table width=\"80%\">"
|
||||
"<tr><td align=\"center\">Start Time</td>"
|
||||
" <td align=\"center\">Average Power</td></tr>";
|
||||
QMapIterator<double,double> j(results);
|
||||
while (j.hasNext()) {
|
||||
j.next();
|
||||
double secs = j.key();
|
||||
double mins = ((int) secs) / 60;
|
||||
secs = secs - mins * 60.0;
|
||||
double hrs = ((int) mins) / 60;
|
||||
mins = mins - hrs * 60.0;
|
||||
QString row =
|
||||
"<tr><td align=\"center\">%1:%2:%3</td>"
|
||||
" <td align=\"center\">%4</td></tr>";
|
||||
row = row.arg(hrs, 0, 'f', 0);
|
||||
row = row.arg(mins, 2, 'f', 0, QLatin1Char('0'));
|
||||
row = row.arg(secs, 2, 'f', 0, QLatin1Char('0'));
|
||||
row = row.arg(j.value(), 0, 'f', 0, QLatin1Char('0'));
|
||||
resultsHtml += row;
|
||||
}
|
||||
resultsHtml += "</table></center>";
|
||||
resultsText->setHtml(resultsHtml);
|
||||
}
|
||||
|
||||
void
|
||||
BestIntervalDialog::doneClicked()
|
||||
{
|
||||
done(0);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* $Id: PowerHist.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
@@ -18,45 +16,31 @@
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_PowerHist_h
|
||||
#define _GC_PowerHist_h 1
|
||||
#ifndef _GC_BestIntervalDialog_h
|
||||
#define _GC_BestIntervalDialog_h 1
|
||||
|
||||
#include <qwt_plot.h>
|
||||
#include <QtGui>
|
||||
|
||||
class QwtPlotCurve;
|
||||
class QwtPlotGrid;
|
||||
class RawFile;
|
||||
class MainWindow;
|
||||
|
||||
class PowerHist : public QwtPlot
|
||||
class BestIntervalDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BestIntervalDialog(MainWindow *mainWindow);
|
||||
|
||||
QwtPlotCurve *curve;
|
||||
private slots:
|
||||
void findClicked();
|
||||
void doneClicked();
|
||||
|
||||
PowerHist();
|
||||
|
||||
int binWidth() const { return binw; }
|
||||
|
||||
void setData(RawFile *raw);
|
||||
|
||||
public slots:
|
||||
|
||||
void setBinWidth(int value);
|
||||
|
||||
protected:
|
||||
|
||||
QwtPlotGrid *grid;
|
||||
|
||||
double *array;
|
||||
int arrayLength;
|
||||
|
||||
int binw;
|
||||
|
||||
void recalc();
|
||||
void setYMax();
|
||||
private:
|
||||
|
||||
MainWindow *mainWindow;
|
||||
QPushButton *findButton, *doneButton;
|
||||
QDoubleSpinBox *hrsSpinBox, *minsSpinBox, *secsSpinBox, *countSpinBox;
|
||||
QTextEdit *resultsText;
|
||||
};
|
||||
|
||||
#endif // _GC_PowerHist_h
|
||||
#endif // _GC_BestIntervalDialog_h
|
||||
|
||||
240
src/BikeScore.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideMetric.h"
|
||||
#include "Zones.h"
|
||||
#include <math.h>
|
||||
|
||||
const double bikeScoreN = 4.0;
|
||||
const double bikeScoreTau = 25.0;
|
||||
|
||||
// NOTE: This code follows the description of xPower, Relative Intensity, and
|
||||
// BikeScore in "Analysis of Power Output and Training Stress in Cyclists: The
|
||||
// Development of the BikeScore(TM) Algorithm", page 5, by Phil Skiba:
|
||||
//
|
||||
// http://www.physfarm.com/Analysis%20of%20Power%20Output%20and%20Training%20Stress%20in%20Cyclists-%20BikeScore.pdf
|
||||
//
|
||||
// The weighting factors for the exponentially weighted average are taken from
|
||||
// a spreadsheet provided by Dr. Skiba.
|
||||
|
||||
class XPower : public RideMetric {
|
||||
double xpower;
|
||||
double secs;
|
||||
|
||||
friend class RelativeIntensity;
|
||||
friend class BikeScore;
|
||||
|
||||
public:
|
||||
|
||||
XPower() : xpower(0.0), secs(0.0) {}
|
||||
QString name() const { return "skiba_xpower"; }
|
||||
QString units(bool) const { return "watts"; }
|
||||
double value(bool) const { return xpower; }
|
||||
void compute(const RideFile *ride, const Zones *, int,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
|
||||
double secsDelta = ride->recIntSecs();
|
||||
|
||||
// djconnel:
|
||||
double attenuation = exp(-secsDelta / bikeScoreTau);
|
||||
double sampleWeight = 1 - attenuation;
|
||||
|
||||
double lastSecs = 0.0; // previous point
|
||||
double initialSecs = 0.0; // time associated with start of data
|
||||
double weighted = 0.0; // exponentially smoothed power
|
||||
double epsilon_time = secsDelta / 100; // for comparison of times
|
||||
|
||||
double total = 0.0;
|
||||
|
||||
/* djconnel: calculate bikescore:
|
||||
For all values of t, smoothed power
|
||||
p* = integral { -infinity to t } (1/tau) exp[(t' - t) / tau] p(t') dt'
|
||||
|
||||
From this we calculate an integral, xw:
|
||||
xw = integral {t0 to t} { p*^N dt }
|
||||
(in the code, p* -> "weighted"; xw -> "total")
|
||||
|
||||
During any interval t0 <= t < t1, with p* = p*(t0) at the start of
|
||||
the interval, with power p constant during the interval:
|
||||
|
||||
p*(t) = p*(t0) exp[(t0 - t) / tau] + p ( 1 - exp[(t0 - t) / tau] )
|
||||
|
||||
So the contribution to xw is then:
|
||||
|
||||
delta_xw = integral { t0 to t1 } [ p*(t0) exp[(t0 - t) / tau] + p ( 1 - exp[(t0 - t) / tau] ) ]^N
|
||||
|
||||
Consider the simplified case p = 0, and t1 = t0 + deltat, then this is evaluated:
|
||||
|
||||
delta_xw = integral { t0 to t1 } ( p*(t0) exp[(t0 - t) / tau] )^N
|
||||
= integral { t0 to t1 } ( p*(t0)^N exp[N (t0 - t) / tau] )
|
||||
= (tau / N) p*(t0)^N (1 - exp[-N deltat / tau])
|
||||
|
||||
This is the component which should be added to xw during idle periods.
|
||||
|
||||
More generally:
|
||||
delta_xw = integral { t0 to t1 } [ p*(t0) exp[(t0 - t) / tau] + p ( 1 - exp[(t0 - t) / tau] ) ]^N
|
||||
= integral { 0 to deltat }
|
||||
[
|
||||
p*(t0)^N exp[-N t' / tau] +
|
||||
N p*(t0)^(N - 1) p exp[-(N - 1) t' / tau] (1 - exp[-t' / tau]) +
|
||||
[N (N - 1) / 2] p*(t0)^(N - 2) p^2 exp[-(N - 2) t' / tau] (1 - exp[-2 t' / tau]) +
|
||||
[N (N - 1) (N - 2) / 6] p*(t0)^(N - 3) p^3 exp[-(N - 3) t' / tau] (1 - exp[-3 t' / tau]) +
|
||||
[N (N - 1) (N - 2) (N - 3) / 24] p*(t0)^(N - 4) p^4 exp[-(N - 4) t' / tau] (1 - exp[-4 t' / tau]) +
|
||||
...
|
||||
] dt'
|
||||
|
||||
but a linearized solution is fine as long as the sampling interval is << the smoothing time.
|
||||
*/
|
||||
|
||||
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
|
||||
int count = 0;
|
||||
while (i.hasNext()) {
|
||||
const RideFilePoint *point = i.next();
|
||||
|
||||
// if there are missing data then add in the contribution
|
||||
// from the exponentially decaying smoothed power
|
||||
if (count == 0)
|
||||
initialSecs = point->secs - secsDelta;
|
||||
else {
|
||||
double dt = point->secs - lastSecs - secsDelta;
|
||||
if (dt > epsilon_time) {
|
||||
double alpha = exp(-bikeScoreN * dt / bikeScoreTau);
|
||||
total +=
|
||||
(bikeScoreTau / bikeScoreN) * pow(weighted, bikeScoreN) * (1 - alpha);
|
||||
weighted *= exp(-dt / bikeScoreTau);
|
||||
}
|
||||
}
|
||||
|
||||
// the existing weighted average is exponentially decayed by one sampling time,
|
||||
// then the contribution from the present point is added
|
||||
weighted = attenuation * weighted + sampleWeight * point->watts;
|
||||
total += pow(weighted, bikeScoreN);
|
||||
|
||||
lastSecs = point->secs;
|
||||
count++;
|
||||
}
|
||||
|
||||
// after the ride is over, assume idleness (exponentially decaying smoothed power) to infinity
|
||||
total +=
|
||||
(bikeScoreTau / bikeScoreN) * pow(weighted, bikeScoreN);
|
||||
|
||||
secs = lastSecs - initialSecs;
|
||||
xpower = (secs > 0) ?
|
||||
pow(total * secsDelta / secs, 1 / bikeScoreN) :
|
||||
0.0;
|
||||
}
|
||||
|
||||
// added djconnel: allow RI to be combined across rides
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
XPower *ap = dynamic_cast<XPower*>(other);
|
||||
xpower = pow(xpower, bikeScoreN) * secs + pow(ap->xpower, bikeScoreN) * ap->secs;
|
||||
secs += ap->secs;
|
||||
xpower = pow(xpower / secs, 1 / bikeScoreN);
|
||||
}
|
||||
// end added djconnel
|
||||
|
||||
RideMetric *clone() const { return new XPower(*this); }
|
||||
};
|
||||
|
||||
class RelativeIntensity : public RideMetric {
|
||||
double reli;
|
||||
double secs;
|
||||
|
||||
public:
|
||||
|
||||
RelativeIntensity() : reli(0.0), secs(0.0) {}
|
||||
QString name() const { return "skiba_relative_intensity"; }
|
||||
QString units(bool) const { return ""; }
|
||||
double value(bool) const { return reli; }
|
||||
void compute(const RideFile *, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) {
|
||||
if (zones) {
|
||||
assert(deps.contains("skiba_xpower"));
|
||||
XPower *xp = dynamic_cast<XPower*>(deps.value("skiba_xpower"));
|
||||
assert(xp);
|
||||
reli = xp->xpower / zones->getCP(zoneRange);
|
||||
secs = xp->secs;
|
||||
}
|
||||
}
|
||||
|
||||
// added djconnel: allow RI to be combined across rides
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
RelativeIntensity *ap = dynamic_cast<RelativeIntensity*>(other);
|
||||
reli = secs * pow(reli, bikeScoreN) + ap->secs * pow(ap->reli, bikeScoreN);
|
||||
secs += ap->secs;
|
||||
reli = pow(reli / secs, 1.0 / bikeScoreN);
|
||||
}
|
||||
// end added djconnel
|
||||
|
||||
RideMetric *clone() const { return new RelativeIntensity(*this); }
|
||||
};
|
||||
|
||||
class BikeScore : public RideMetric {
|
||||
double score;
|
||||
|
||||
public:
|
||||
|
||||
BikeScore() : score(0.0) {}
|
||||
QString name() const { return "skiba_bike_score"; }
|
||||
QString units(bool) const { return ""; }
|
||||
double value(bool) const { return score; }
|
||||
void compute(const RideFile *ride, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) {
|
||||
if (!zones)
|
||||
return;
|
||||
if (ride->deviceType() == QString("Manual CSV")) {
|
||||
// manual entry, use BS from dataPoints
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
const RideFilePoint *point = i.next();
|
||||
score = point->bs;
|
||||
}
|
||||
else {
|
||||
assert(deps.contains("skiba_xpower"));
|
||||
assert(deps.contains("skiba_relative_intensity"));
|
||||
XPower *xp = dynamic_cast<XPower*>(deps.value("skiba_xpower"));
|
||||
RideMetric *ri = deps.value("skiba_relative_intensity");
|
||||
assert(ri);
|
||||
double normWork = xp->xpower * xp->secs;
|
||||
double rawBikeScore = normWork * ri->value(true);
|
||||
double workInAnHourAtCP = zones->getCP(zoneRange) * 3600;
|
||||
score = rawBikeScore / workInAnHourAtCP * 100.0;
|
||||
}
|
||||
}
|
||||
RideMetric *clone() const { return new BikeScore(*this); }
|
||||
bool canAggregate() const { return true; }
|
||||
void aggregateWith(RideMetric *other) { score += other->value(true); }
|
||||
};
|
||||
|
||||
static bool addAllThree() {
|
||||
RideMetricFactory::instance().addMetric(XPower());
|
||||
QVector<QString> deps;
|
||||
deps.append("skiba_xpower");
|
||||
RideMetricFactory::instance().addMetric(RelativeIntensity(), &deps);
|
||||
deps.append("skiba_relative_intensity");
|
||||
RideMetricFactory::instance().addMetric(BikeScore(), &deps);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool allThreeAdded = addAllThree();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: ChooseCyclistDialog.cpp,v 1.3 2006/07/04 12:55:40 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: ChooseCyclistDialog.h,v 1.3 2006/07/04 12:55:40 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
47
src/CommPort.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "CommPort.h"
|
||||
#include "Serial.h"
|
||||
|
||||
static QVector<CommPort::ListFunction> *listFunctions;
|
||||
|
||||
bool
|
||||
CommPort::addListFunction(ListFunction f)
|
||||
{
|
||||
if (!listFunctions)
|
||||
listFunctions = new QVector<CommPort::ListFunction>;
|
||||
listFunctions->append(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<CommPortPtr>
|
||||
CommPort::listCommPorts(QString &err)
|
||||
{
|
||||
err = "";
|
||||
QVector<CommPortPtr> result;
|
||||
for (int i = 0; listFunctions && i < listFunctions->size(); ++i) {
|
||||
QVector<CommPortPtr> tmp = (*listFunctions)[i](err);
|
||||
if (err == "")
|
||||
result << tmp;
|
||||
else
|
||||
err += "\n";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
46
src/CommPort.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_CommPort_h
|
||||
#define _GC_CommPort_h 1
|
||||
|
||||
#include <QtCore>
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
class CommPort;
|
||||
typedef boost::shared_ptr<CommPort> CommPortPtr;
|
||||
|
||||
class CommPort
|
||||
{
|
||||
public:
|
||||
|
||||
typedef QVector<CommPortPtr> (*ListFunction)(QString &err);
|
||||
static bool addListFunction(ListFunction f);
|
||||
static QVector<CommPortPtr> listCommPorts(QString &err);
|
||||
|
||||
virtual ~CommPort() {}
|
||||
virtual bool open(QString &err) = 0;
|
||||
virtual void close() = 0;
|
||||
virtual int read(void *buf, size_t nbyte, QString &err) = 0;
|
||||
virtual int write(void *buf, size_t nbyte, QString &err) = 0;
|
||||
virtual QString name() const = 0;
|
||||
|
||||
};
|
||||
|
||||
#endif // _GC_CommPort_h
|
||||
|
||||
240
src/ConfigDialog.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
#include <QtGui>
|
||||
#include <QSettings>
|
||||
#include <assert.h>
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "ConfigDialog.h"
|
||||
#include "Pages.h"
|
||||
#include "Settings.h"
|
||||
#include "Zones.h"
|
||||
|
||||
|
||||
/* cyclist dialog protocol redesign:
|
||||
* no zones:
|
||||
* calendar disabled
|
||||
* automatically go into "new" mode
|
||||
* zone(s) defined:
|
||||
* click on calendar: sets current zone to that associated with date
|
||||
* save clicked:
|
||||
* if new mode, create a new zone starting at selected date, or for all dates
|
||||
* if this is only zone.
|
||||
* delete clicked:
|
||||
* deletes currently selected zone
|
||||
*/
|
||||
|
||||
ConfigDialog::ConfigDialog(QDir _home, Zones **_zones)
|
||||
{
|
||||
|
||||
home = _home;
|
||||
zones = _zones;
|
||||
|
||||
assert(zones);
|
||||
|
||||
cyclistPage = new CyclistPage(zones);
|
||||
|
||||
contentsWidget = new QListWidget;
|
||||
contentsWidget->setViewMode(QListView::IconMode);
|
||||
contentsWidget->setIconSize(QSize(96, 84));
|
||||
contentsWidget->setMovement(QListView::Static);
|
||||
contentsWidget->setMinimumWidth(128);
|
||||
contentsWidget->setMaximumWidth(128);
|
||||
contentsWidget->setMinimumHeight(128);
|
||||
contentsWidget->setSpacing(12);
|
||||
|
||||
configPage = new ConfigurationPage();
|
||||
|
||||
pagesWidget = new QStackedWidget;
|
||||
pagesWidget->addWidget(configPage);
|
||||
pagesWidget->addWidget(cyclistPage);
|
||||
|
||||
closeButton = new QPushButton(tr("Close"));
|
||||
saveButton = new QPushButton(tr("Save"));
|
||||
|
||||
createIcons();
|
||||
contentsWidget->setCurrentItem(contentsWidget->item(0));
|
||||
|
||||
// connect(closeButton, SIGNAL(clicked()), this, SLOT(reject()));
|
||||
// connect(saveButton, SIGNAL(clicked()), this, SLOT(accept()));
|
||||
connect(closeButton, SIGNAL(clicked()), this, SLOT(accept()));
|
||||
connect(cyclistPage->btnBack, SIGNAL(clicked()), this, SLOT(back_Clicked()));
|
||||
connect(cyclistPage->btnForward, SIGNAL(clicked()), this, SLOT(forward_Clicked()));
|
||||
connect(cyclistPage->btnDelete, SIGNAL(clicked()), this, SLOT(delete_Clicked()));
|
||||
connect(cyclistPage->calendar, SIGNAL(selectionChanged()), this, SLOT(calendarDateChanged()));
|
||||
|
||||
horizontalLayout = new QHBoxLayout;
|
||||
horizontalLayout->addWidget(contentsWidget);
|
||||
horizontalLayout->addWidget(pagesWidget, 1);
|
||||
|
||||
buttonsLayout = new QHBoxLayout;
|
||||
buttonsLayout->addStretch(1);
|
||||
buttonsLayout->addWidget(closeButton);
|
||||
buttonsLayout->addWidget(saveButton);
|
||||
|
||||
mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(horizontalLayout);
|
||||
mainLayout->addStretch(1);
|
||||
mainLayout->addSpacing(12);
|
||||
mainLayout->addLayout(buttonsLayout);
|
||||
setLayout(mainLayout);
|
||||
|
||||
setWindowTitle(tr("Config Dialog"));
|
||||
}
|
||||
|
||||
ConfigDialog::~ConfigDialog()
|
||||
{
|
||||
delete cyclistPage;
|
||||
delete contentsWidget;
|
||||
delete configPage;
|
||||
delete pagesWidget;
|
||||
delete closeButton;
|
||||
delete horizontalLayout;
|
||||
delete buttonsLayout;
|
||||
delete mainLayout;
|
||||
}
|
||||
|
||||
|
||||
void ConfigDialog::createIcons()
|
||||
{
|
||||
QListWidgetItem *configButton = new QListWidgetItem(contentsWidget);
|
||||
configButton->setIcon(QIcon(":/images/config.png"));
|
||||
configButton->setText(tr("Configuration"));
|
||||
configButton->setTextAlignment(Qt::AlignHCenter);
|
||||
configButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
|
||||
|
||||
QListWidgetItem *cyclistButton = new QListWidgetItem(contentsWidget);
|
||||
cyclistButton->setIcon(QIcon(":images/cyclist.png"));
|
||||
cyclistButton->setText(tr("Cyclist Info"));
|
||||
cyclistButton->setTextAlignment(Qt::AlignHCenter);
|
||||
cyclistButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
|
||||
|
||||
connect(contentsWidget,
|
||||
SIGNAL(currentItemChanged(QListWidgetItem *, QListWidgetItem *)),
|
||||
this, SLOT(changePage(QListWidgetItem *, QListWidgetItem*)));
|
||||
|
||||
connect(saveButton, SIGNAL(clicked()), this, SLOT(save_Clicked()));
|
||||
}
|
||||
|
||||
|
||||
void ConfigDialog::createNewRange()
|
||||
{
|
||||
}
|
||||
|
||||
void ConfigDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous)
|
||||
{
|
||||
if (!current)
|
||||
current = previous;
|
||||
|
||||
pagesWidget->setCurrentIndex(contentsWidget->row(current));
|
||||
}
|
||||
|
||||
// if save is clicked, we want to:
|
||||
// new mode: create a new zone starting at the selected date (which may be null, implying BEGIN
|
||||
// ! new mode: change the CP associated with the present mode
|
||||
void ConfigDialog::save_Clicked()
|
||||
{
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
settings->setValue(GC_UNIT, configPage->unitCombo->currentText());
|
||||
settings->setValue(GC_ALLRIDES_ASCENDING, configPage->allRidesAscending->checkState());
|
||||
settings->setValue(GC_CRANKLENGTH, configPage->crankLengthCombo->currentText());
|
||||
settings->setValue(GC_BIKESCOREDAYS, configPage->BSdaysEdit->text());
|
||||
settings->setValue(GC_BIKESCOREMODE, configPage->bsModeCombo->currentText());
|
||||
|
||||
// if the CP text entry reads invalid, there's nothing we can do
|
||||
int cp = cyclistPage->getCP();
|
||||
if (cp == 0) {
|
||||
QMessageBox::warning(this, tr("Invalid CP"), "Please enter valid CP and try again.");
|
||||
cyclistPage->setCPFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// if for some reason we have no zones yet, then create them
|
||||
int range;
|
||||
assert(zones);
|
||||
if (! *zones) {
|
||||
*zones = new Zones();
|
||||
range = -1;
|
||||
}
|
||||
else
|
||||
// determine the current range
|
||||
range = cyclistPage->getCurrentRange();
|
||||
|
||||
// if this is new mode, or if no zone ranges are yet defined, set up the new range
|
||||
if ((range == -1) || (cyclistPage->isNewMode()))
|
||||
cyclistPage->setCurrentRange(range = (*zones)->insertRangeAtDate(cyclistPage->selectedDate(), cp));
|
||||
else
|
||||
(*zones)->setCP(range, cyclistPage->getText().toInt());
|
||||
|
||||
(*zones)->setZonesFromCP(range);
|
||||
|
||||
// update the "new zone" checkbox to visible and unchecked
|
||||
cyclistPage->checkboxNew->setChecked(Qt::Unchecked);
|
||||
cyclistPage->checkboxNew->setEnabled(true);
|
||||
|
||||
(*zones)->write(home);
|
||||
}
|
||||
|
||||
void ConfigDialog::moveCalendarToCurrentRange() {
|
||||
int range = cyclistPage->getCurrentRange();
|
||||
|
||||
if (range < 0)
|
||||
return;
|
||||
|
||||
QDate date;
|
||||
|
||||
// put the cursor at the beginning of the selected range if it's not the first
|
||||
if (range > 0)
|
||||
date = (*zones)->getStartDate(cyclistPage->getCurrentRange());
|
||||
// unless the range is the first range, in which case it goes at the end of that range
|
||||
// use JulianDay to subtract one day from the end date, which is actually the first
|
||||
// day of the following range
|
||||
else
|
||||
date = QDate::fromJulianDay((*zones)->getEndDate(cyclistPage->getCurrentRange()).toJulianDay() - 1);
|
||||
|
||||
cyclistPage->setSelectedDate(date);
|
||||
}
|
||||
|
||||
void ConfigDialog::back_Clicked()
|
||||
{
|
||||
QDate date;
|
||||
cyclistPage->setCurrentRange(cyclistPage->getCurrentRange() - 1);
|
||||
moveCalendarToCurrentRange();
|
||||
}
|
||||
|
||||
void ConfigDialog::forward_Clicked()
|
||||
{
|
||||
QDate date;
|
||||
cyclistPage->setCurrentRange(cyclistPage->getCurrentRange() + 1);
|
||||
moveCalendarToCurrentRange();
|
||||
}
|
||||
|
||||
void ConfigDialog::delete_Clicked() {
|
||||
int range = cyclistPage->getCurrentRange();
|
||||
int num_ranges = (*zones)->getRangeSize();
|
||||
assert (num_ranges > 1);
|
||||
QMessageBox msgBox;
|
||||
msgBox.setText(
|
||||
tr("Are you sure you want to delete the zone range\n"
|
||||
"from %1 to %2?\n"
|
||||
"(%3 range will extend to this date range):") .
|
||||
arg((*zones)->getStartDateString(cyclistPage->getCurrentRange())) .
|
||||
arg((*zones)->getEndDateString(cyclistPage->getCurrentRange())) .
|
||||
arg((range > 0) ? "previous" : "next")
|
||||
);
|
||||
QPushButton *deleteButton = msgBox.addButton(tr("Delete"),QMessageBox::YesRole);
|
||||
msgBox.setStandardButtons(QMessageBox::Cancel);
|
||||
msgBox.setDefaultButton(QMessageBox::Cancel);
|
||||
msgBox.setIcon(QMessageBox::Critical);
|
||||
msgBox.exec();
|
||||
if(msgBox.clickedButton() == deleteButton)
|
||||
cyclistPage->setCurrentRange((*zones)->deleteRange(range));
|
||||
|
||||
(*zones)->write(home);
|
||||
}
|
||||
|
||||
void ConfigDialog::calendarDateChanged() {
|
||||
int range = (*zones)->whichRange(cyclistPage->selectedDate());
|
||||
assert(range >= 0);
|
||||
cyclistPage->setCurrentRange(range);
|
||||
}
|
||||
50
src/ConfigDialog.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef CONFIGDIALOG_H
|
||||
#define CONFIGDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QSettings>
|
||||
#include "Pages.h"
|
||||
#include "MainWindow.h"
|
||||
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QStackedWidget;
|
||||
class Zones;
|
||||
|
||||
class ConfigDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ConfigDialog(QDir home, Zones **zones);
|
||||
~ConfigDialog();
|
||||
|
||||
public slots:
|
||||
void changePage(QListWidgetItem *current, QListWidgetItem *previous);
|
||||
void save_Clicked();
|
||||
void back_Clicked();
|
||||
void forward_Clicked();
|
||||
void delete_Clicked();
|
||||
void calendarDateChanged();
|
||||
|
||||
private:
|
||||
void createIcons();
|
||||
void calculateZones();
|
||||
void createNewRange();
|
||||
void moveCalendarToCurrentRange();
|
||||
|
||||
ConfigurationPage *configPage;
|
||||
CyclistPage *cyclistPage;
|
||||
QPushButton *saveButton;
|
||||
QStackedWidget *pagesWidget;
|
||||
QPushButton *closeButton;
|
||||
QHBoxLayout *horizontalLayout;
|
||||
QHBoxLayout *buttonsLayout;
|
||||
QVBoxLayout *mainLayout;
|
||||
QListWidget *contentsWidget;
|
||||
|
||||
QSettings *settings;
|
||||
QDir home;
|
||||
Zones **zones;
|
||||
};
|
||||
|
||||
#endif
|
||||
815
src/CpintPlot.cpp
Normal file
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "Zones.h"
|
||||
#include "CpintPlot.h"
|
||||
#include <assert.h>
|
||||
#include <unistd.h>
|
||||
#include <QDebug>
|
||||
#include <qwt_data.h>
|
||||
#include <qwt_legend.h>
|
||||
#include <qwt_plot_curve.h>
|
||||
#include <qwt_plot_grid.h>
|
||||
#include "RideItem.h"
|
||||
#include "LogTimeScaleDraw.h"
|
||||
#include "LogTimeScaleEngine.h"
|
||||
#include "RideFile.h"
|
||||
|
||||
CpintPlot::CpintPlot(
|
||||
QString p
|
||||
) :
|
||||
progress(NULL),
|
||||
needToScanRides(true),
|
||||
path(p),
|
||||
thisCurve(NULL),
|
||||
CPCurve(NULL),
|
||||
grid(NULL),
|
||||
zones(NULL)
|
||||
{
|
||||
insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
|
||||
setCanvasBackground(Qt::white);
|
||||
setAxisTitle(yLeft, "Average Power (watts)");
|
||||
setAxisTitle(xBottom, "Interval Length");
|
||||
setAxisScaleDraw(xBottom, new LogTimeScaleDraw);
|
||||
setAxisScaleEngine(xBottom, new LogTimeScaleEngine);
|
||||
setAxisScale(xBottom, 1.0 / 60.0, 60);
|
||||
|
||||
grid = new QwtPlotGrid();
|
||||
grid->enableX(false);
|
||||
QPen gridPen;
|
||||
gridPen.setStyle(Qt::DotLine);
|
||||
grid->setPen(gridPen);
|
||||
grid->attach(this);
|
||||
|
||||
allCurves= QList <QwtPlotCurve *>::QList();
|
||||
allZoneLabels= QList <QwtPlotMarker *>::QList();
|
||||
}
|
||||
|
||||
struct cpi_file_info {
|
||||
QString file, inname, outname;
|
||||
};
|
||||
|
||||
bool
|
||||
is_ride_filename(const QString filename)
|
||||
{
|
||||
QRegExp re("^([0-9][0-9][0-9][0-9])_([0-9][0-9])_([0-9][0-9])"
|
||||
"_([0-9][0-9])_([0-9][0-9])_([0-9][0-9])\\.(raw|srm|csv|tcx|hrm|wko)$");
|
||||
return (re.exactMatch(filename));
|
||||
}
|
||||
|
||||
QString
|
||||
ride_filename_to_cpi_filename(const QString filename)
|
||||
{
|
||||
return (QFileInfo(filename).completeBaseName() + ".cpi");
|
||||
}
|
||||
|
||||
static void
|
||||
cpi_files_to_update(const QDir &dir, QList<cpi_file_info> &result)
|
||||
{
|
||||
QStringList filenames = RideFileFactory::instance().listRideFiles(dir);
|
||||
QListIterator<QString> i(filenames);
|
||||
while (i.hasNext()) {
|
||||
const QString &filename = i.next();
|
||||
if (is_ride_filename(filename)) {
|
||||
QString inname = dir.absoluteFilePath(filename);
|
||||
QString outname =
|
||||
dir.absoluteFilePath(ride_filename_to_cpi_filename(filename));
|
||||
QFileInfo ifi(inname), ofi(outname);
|
||||
if (!ofi.exists() || (ofi.lastModified() < ifi.lastModified())) {
|
||||
cpi_file_info info;
|
||||
info.file = filename;
|
||||
info.inname = inname;
|
||||
info.outname = outname;
|
||||
result.append(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct cpint_point
|
||||
{
|
||||
double secs;
|
||||
int watts;
|
||||
cpint_point(double s, int w) : secs(s), watts(w) {}
|
||||
};
|
||||
|
||||
struct cpint_data {
|
||||
QStringList errors;
|
||||
QList<cpint_point> points;
|
||||
int rec_int_ms;
|
||||
cpint_data() : rec_int_ms(0) {}
|
||||
};
|
||||
|
||||
static void
|
||||
update_cpi_file(const cpi_file_info *info, QProgressDialog *progress,
|
||||
double &progress_sum, double progress_max)
|
||||
{
|
||||
QFile file(info->inname);
|
||||
QStringList errors;
|
||||
RideFile *rideFile =
|
||||
RideFileFactory::instance().openRideFile(file, errors);
|
||||
if (! rideFile)
|
||||
return;
|
||||
cpint_data data;
|
||||
data.rec_int_ms = (int) round(rideFile->recIntSecs() * 1000.0);
|
||||
QListIterator<RideFilePoint*> i(rideFile->dataPoints());
|
||||
while (i.hasNext()) {
|
||||
const RideFilePoint *p = i.next();
|
||||
double secs = round(p->secs * 1000.0) / 1000;
|
||||
data.points.append(cpint_point(secs, (int) round(p->watts)));
|
||||
}
|
||||
delete rideFile;
|
||||
|
||||
FILE *out = fopen(info->outname.toAscii().constData(), "w");
|
||||
assert(out);
|
||||
|
||||
int total_secs = (int) ceil(data.points.back().secs);
|
||||
|
||||
// don't allow data more than one week
|
||||
#define SECONDS_PER_WEEK 7 * 24 * 60 * 60
|
||||
if (total_secs > SECONDS_PER_WEEK) {
|
||||
fclose(out);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
QVector <double> ride_bests(total_secs + 1); // was calloc'ed array (unfreed?), changed djconnel
|
||||
|
||||
// initialize ride_bests
|
||||
for (int i = 0; i < ride_bests.size(); i ++)
|
||||
ride_bests[i] = 0.0;
|
||||
|
||||
bool canceled = false;
|
||||
int progress_count = 0;
|
||||
for (int i = 0; i < data.points.size() - 1; ++i) {
|
||||
cpint_point *p = &data.points[i];
|
||||
double sum = 0.0;
|
||||
double prev_secs = p->secs;
|
||||
for (int j = i + 1; j < data.points.size(); ++j) {
|
||||
cpint_point *q = &data.points[j];
|
||||
if (++progress_count % 1000 == 0) {
|
||||
double x = (progress_count + progress_sum)
|
||||
/ progress_max * 1000.0;
|
||||
// Use min() just in case math is wrong...
|
||||
int n = qMin((int) round(x), 1000);
|
||||
progress->setValue(n);
|
||||
QCoreApplication::processEvents();
|
||||
if (progress->wasCanceled()) {
|
||||
canceled = true;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
sum += data.rec_int_ms / 1000.0 * q->watts;
|
||||
double dur_secs = q->secs - p->secs;
|
||||
double avg = sum / dur_secs;
|
||||
int dur_secs_top = (int) floor(dur_secs);
|
||||
int dur_secs_bot =
|
||||
qMax((int) floor(dur_secs - data.rec_int_ms / 1000.0), 0);
|
||||
for (int k = dur_secs_top; k > dur_secs_bot; --k) {
|
||||
if (ride_bests[k] < avg)
|
||||
ride_bests[k] = avg;
|
||||
}
|
||||
prev_secs = q->secs;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid decreasing work with increasing duration
|
||||
{
|
||||
double maxwork = 0.0;
|
||||
for (int i = 1; i <= total_secs; ++i) {
|
||||
// note index is being used here in lieu of time, as the index
|
||||
// is assumed to be proportional to time
|
||||
double work = ride_bests[i] * i;
|
||||
if (maxwork > work)
|
||||
ride_bests[i] = round(maxwork / i);
|
||||
else
|
||||
maxwork = work;
|
||||
if (ride_bests[i] != 0)
|
||||
fprintf(out, "%6.3f %3.0f\n", i / 60.0, round(ride_bests[i]));
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
fclose(out);
|
||||
if (canceled)
|
||||
unlink(info->outname.toAscii().constData());
|
||||
progress_sum += progress_count;
|
||||
}
|
||||
|
||||
QDate
|
||||
cpi_filename_to_date(const QString filename) {
|
||||
QRegExp rx(".*([0-9][0-9][0-9][0-9])_([0-9][0-9])_([0-9][0-9])"
|
||||
"_([0-9][0-9])_([0-9][0-9])_([0-9][0-9])\\.cpi$");
|
||||
if (rx.exactMatch(filename)) {
|
||||
assert(rx.numCaptures() == 6);
|
||||
QDate date = QDate(
|
||||
rx.cap(1).toInt(),
|
||||
rx.cap(2).toInt(),
|
||||
rx.cap(3).toInt()
|
||||
);
|
||||
|
||||
if (! date.isValid()) {
|
||||
return QDate();
|
||||
}
|
||||
else
|
||||
return date;
|
||||
}
|
||||
else
|
||||
return QDate(); // return value was 1 Jan: changed to null
|
||||
}
|
||||
|
||||
static int
|
||||
read_one(const char *inname, QVector<double> &bests, QVector<QDate> &bestDates, QHash <QString, bool> *cpiDataInBests)
|
||||
{
|
||||
FILE *in = fopen(inname, "r");
|
||||
if (!in)
|
||||
return -1;
|
||||
int lineno = 1;
|
||||
char line[40];
|
||||
|
||||
while (fgets(line, sizeof(line), in) != NULL) {
|
||||
double mins;
|
||||
int watts;
|
||||
if (sscanf(line, "%lf %d\n", &mins, &watts) != 2) {
|
||||
fprintf(stderr, "Bad match on line %d: %s", lineno, line);
|
||||
exit(1);
|
||||
}
|
||||
int secs = (int) round(mins * 60.0);
|
||||
if (secs >= bests.size()) {
|
||||
bests.resize(secs + 1);
|
||||
bestDates.resize(secs + 1);
|
||||
}
|
||||
if (bests[secs] < watts){
|
||||
bests[secs] = watts;
|
||||
bestDates[secs] = cpi_filename_to_date(QString(inname));
|
||||
|
||||
// mark the filename as having contributed to the bests
|
||||
// Note this contribution may subsequently be over-written, so
|
||||
// for example the first file scanned will always be tagged.
|
||||
if (cpiDataInBests)
|
||||
(*cpiDataInBests)[inname] = true;
|
||||
}
|
||||
++lineno;
|
||||
}
|
||||
fclose(in);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
read_cpi_file(const QDir &dir, const QFileInfo &raw, QVector<double> &bests, QVector<QDate> &bestDates, QHash <QString, bool> *cpiDataInBests)
|
||||
{
|
||||
QString inname = dir.absoluteFilePath(raw.completeBaseName() + ".cpi");
|
||||
return read_one(inname.toAscii().constData(), bests, bestDates, cpiDataInBests);
|
||||
}
|
||||
|
||||
// extract critical power parameters which match the given curve
|
||||
// model: maximal power = cp (1 + tau / [t + t0]), where t is the
|
||||
// duration of the effort, and t, cp and tau are model parameters
|
||||
// the basic critical power model is t0 = 0, but non-zero has
|
||||
// been discussed in the literature
|
||||
// it is assumed duration = index * seconds
|
||||
void
|
||||
CpintPlot::deriveCPParameters()
|
||||
{
|
||||
// bounds on anaerobic interval in minutes
|
||||
const double t1 = USE_T0_IN_CP_MODEL ? 0.25 : 1;
|
||||
const double t2 = 6;
|
||||
|
||||
// bounds on aerobic interval in minutes
|
||||
const double t3 = 10;
|
||||
const double t4 = 60;
|
||||
|
||||
// bounds of these time valus in the data
|
||||
int i1, i2, i3, i4;
|
||||
|
||||
// find the indexes associated with the bounds
|
||||
// the first point must be at least the minimum for the anaerobic interval, or quit
|
||||
for (i1 = 0; i1 < 60 * t1; i1++)
|
||||
if (i1 + 1 >= bests.size())
|
||||
return;
|
||||
// the second point is the maximum point suitable for anaerobicly dominated efforts.
|
||||
for (i2 = i1; i2 + 1 <= 60 * t2; i2++)
|
||||
if (i2 + 1 >= bests.size())
|
||||
return;
|
||||
// the third point is the beginning of the minimum duration for aerobic efforts
|
||||
for (i3 = i2; i3 < 60 * t3; i3++)
|
||||
if (i3 + 1 >= bests.size())
|
||||
return;
|
||||
for (i4 = i3; i4 + 1 <= 60 * t4; i4++)
|
||||
if (i4 + 1 >= bests.size())
|
||||
break;
|
||||
|
||||
// initial estimate of tau
|
||||
if (tau == 0)
|
||||
tau = 1;
|
||||
|
||||
// initial estimate of cp (if not already available)
|
||||
if (cp == 0)
|
||||
cp = 300;
|
||||
|
||||
// initial estimate of t0: start small to maximize sensitivity to data
|
||||
t0 = 0;
|
||||
|
||||
// lower bound on tau
|
||||
const double tau_min = 0.5;
|
||||
|
||||
// convergence delta for tau
|
||||
const double tau_delta_max = 1e-4;
|
||||
const double t0_delta_max = 1e-4;
|
||||
|
||||
// previous loop value of tau and t0
|
||||
double tau_prev;
|
||||
double t0_prev;
|
||||
|
||||
// maximum number of loops
|
||||
const int max_loops = 100;
|
||||
|
||||
// loop to convergence
|
||||
int iteration = 0;
|
||||
do {
|
||||
if (iteration ++ > max_loops) {
|
||||
fprintf(stderr, "maximum number of loops %d exceeded in cp model extraction\n", max_loops);
|
||||
break;
|
||||
}
|
||||
|
||||
// record the previous version of tau, for convergence
|
||||
tau_prev = tau;
|
||||
t0_prev = t0;
|
||||
|
||||
// estimate cp, given tau
|
||||
int i;
|
||||
cp = 0;
|
||||
for (i = i3; i <= i4; i++) {
|
||||
double cpn = bests[i] / (1 + tau / (t0 + i / 60.0));
|
||||
if (cp < cpn)
|
||||
cp = cpn;
|
||||
}
|
||||
|
||||
// if cp = 0; no valid data; give up
|
||||
if (cp == 0.0)
|
||||
return;
|
||||
|
||||
// estimate tau, given cp
|
||||
tau = tau_min;
|
||||
for (i = i1; i <= i2; i++) {
|
||||
double taun = (bests[i] / cp - 1) * (i / 60.0 + t0) - t0;
|
||||
if (tau < taun)
|
||||
tau = taun;
|
||||
}
|
||||
|
||||
// update t0 if we're using that model
|
||||
#if USE_T0_IN_CP_MODEL
|
||||
t0 = tau / (bests[1] / cp - 1) - 1 / 60.0;
|
||||
#endif
|
||||
|
||||
// the following line is debugging code and can be removed
|
||||
fprintf(stderr, "%d: tau = %.2f; cp = %.2f; t0 = %.2f\n", iteration, tau, cp, t0);
|
||||
|
||||
} while ((fabs(tau - tau_prev) > tau_delta_max) ||
|
||||
(fabs(t0 - t0_prev) > t0_delta_max)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
void
|
||||
CpintPlot::plot_CP_curve(
|
||||
CpintPlot *thisPlot, // the plot we're currently displaying
|
||||
double cp,
|
||||
double tau,
|
||||
double t0
|
||||
) {
|
||||
|
||||
// detach the CP curve if it exists
|
||||
if (CPCurve)
|
||||
CPCurve->detach();
|
||||
|
||||
// if there's no cp, then there's nothing to do
|
||||
if (cp <= 0)
|
||||
return;
|
||||
|
||||
// populate curve data with a CP curve
|
||||
const int curve_points = 100;
|
||||
double tmin = USE_T0_IN_CP_MODEL ? 1.0/60 : tau;
|
||||
double tmax = 180.0;
|
||||
double cp_curve_power[curve_points];
|
||||
double cp_curve_time[curve_points];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < curve_points; i ++) {
|
||||
double x = (double) i / (curve_points - 1);
|
||||
double t = pow(tmax, x) * pow(tmin, 1-x);
|
||||
cp_curve_time[i] = t;
|
||||
cp_curve_power[i] = cp * (1 + tau / (t + t0));
|
||||
}
|
||||
|
||||
// generate a plot
|
||||
QString curve_title;
|
||||
#if USE_T0_IN_CP_MODEL
|
||||
curve_title.sprintf("CP=%.1f W; AWC/CP=%.2f m; t0=%.1f s", cp, tau, 60 * t0);
|
||||
#else
|
||||
curve_title.sprintf("CP=%.1f W; AWC/CP=%.2f m", cp, tau);
|
||||
#endif
|
||||
|
||||
CPCurve = new QwtPlotCurve(curve_title);
|
||||
CPCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
QPen *pen = new QPen(Qt::red);
|
||||
pen->setWidth(2.0);
|
||||
pen->setStyle(Qt::DashLine);
|
||||
CPCurve->setPen(*pen);
|
||||
CPCurve->setData(
|
||||
cp_curve_time,
|
||||
cp_curve_power,
|
||||
curve_points
|
||||
);
|
||||
CPCurve->attach(thisPlot);
|
||||
delete pen;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void CpintPlot::clear_CP_Curves() {
|
||||
// unattach any existing shading curves and reset the list
|
||||
if (allCurves.size()) {
|
||||
QListIterator<QwtPlotCurve *> i(allCurves);
|
||||
while (i.hasNext()) {
|
||||
QwtPlotCurve *curve = i.next();
|
||||
if (curve) {
|
||||
curve->detach();
|
||||
delete curve;
|
||||
}
|
||||
}
|
||||
allCurves.clear();
|
||||
}
|
||||
|
||||
// now delete any labels
|
||||
if (allZoneLabels.size()) {
|
||||
QListIterator<QwtPlotMarker *> i(allZoneLabels);
|
||||
while (i.hasNext()) {
|
||||
QwtPlotMarker *label = i.next();
|
||||
if (label) {
|
||||
label->detach();
|
||||
delete label;
|
||||
}
|
||||
}
|
||||
allZoneLabels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
CpintPlot::plot_allCurve (
|
||||
CpintPlot *thisPlot,
|
||||
int n_values,
|
||||
const double *power_values
|
||||
) {
|
||||
|
||||
clear_CP_Curves();
|
||||
|
||||
QVector<double> time_values(n_values);
|
||||
// generate an array of time values
|
||||
//double time_values[n_values];
|
||||
for (int t = 1; t <= n_values; t++)
|
||||
time_values[t - 1] = t / 60.0;
|
||||
|
||||
// generate zones from derived CP value
|
||||
if (cp > 0) {
|
||||
QList <int> power_zone;
|
||||
int n_zones = (*zones)->lowsFromCP(&power_zone, (int) int(cp));
|
||||
|
||||
QList <int> n_zone;
|
||||
|
||||
// the lowest zone goes to zero power, so mark its start at the last data point
|
||||
n_zone.append(n_values - 1);
|
||||
|
||||
// start the search at the next-to-lowest zone
|
||||
int z = 1;
|
||||
|
||||
// search the maximal power curve to extract the zone times
|
||||
for (int i = n_values; i-- > 0;) {
|
||||
// if we reach the beginning of the curve OR if we hit a zone boundary, we're done with the present zone
|
||||
if ((i == 0) || (power_values[i] > power_zone[z])) {
|
||||
n_zone.append(
|
||||
(z == n_zones) ?
|
||||
0 :
|
||||
(
|
||||
(
|
||||
(i == n_values - 1) ||
|
||||
(abs(power_values[i] - power_zone[z]) < abs(power_zone[z] - power_values[i + 1]))
|
||||
) ?
|
||||
i :
|
||||
i + 1
|
||||
)
|
||||
);
|
||||
|
||||
// draw curves for the zone we're leaving, if it spans any segments
|
||||
if (n_zone[z - 1] > n_zone[z]) {
|
||||
// define the individual code segments. Note in the old code with a single segment, it was
|
||||
// part of the class. This curve is not a protected member of the class. djconnel Apr2009
|
||||
QwtPlotCurve *curve;
|
||||
curve =
|
||||
new QwtPlotCurve((*zones)->getDefaultZoneName(z - 1));
|
||||
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
QPen *pen = new QPen(zoneColor(z - 1, n_zones));
|
||||
pen->setWidth(2.0);
|
||||
curve->setPen(*pen);
|
||||
curve->attach(thisPlot);
|
||||
QColor brush_color = zoneColor(z - 1, n_zones);
|
||||
brush_color.setAlpha(64);
|
||||
curve->setBrush(brush_color); // brush fills below the line
|
||||
curve->setData(
|
||||
time_values.data() + n_zone[z],
|
||||
power_values + n_zone[z],
|
||||
n_zone[z - 1] - n_zone[z] + 1
|
||||
);
|
||||
delete pen;
|
||||
|
||||
// add the curve to the list
|
||||
allCurves.append(curve);
|
||||
|
||||
// render a colored label on the zone
|
||||
QwtText text((*zones)->getDefaultZoneName(z - 1));
|
||||
text.setFont(QFont("Helvetica",24, QFont::Bold));
|
||||
QColor text_color = zoneColor(z - 1, n_zones);
|
||||
text_color.setAlpha(128);
|
||||
text.setColor(text_color);
|
||||
QwtPlotMarker *label_mark;
|
||||
label_mark = new QwtPlotMarker();
|
||||
|
||||
// place the text in the geometric mean in time, at a decent power
|
||||
label_mark->setValue(
|
||||
sqrt(time_values[n_zone[z-1]] * time_values[n_zone[z]]),
|
||||
(power_values[n_zone[z-1]] + power_values[n_zone[z]]) / 5
|
||||
);
|
||||
label_mark->setLabel(text);
|
||||
label_mark->attach(thisPlot);
|
||||
allZoneLabels.append(label_mark);
|
||||
}
|
||||
|
||||
if (z < n_zones)
|
||||
fprintf(stderr, "zone %s: %d watts, index = %d\n",
|
||||
(*zones)->getDefaultZoneName(z).toAscii().constData(),
|
||||
power_zone[z],
|
||||
n_zone[z]
|
||||
);
|
||||
|
||||
// if we're to the smallest time, we're done
|
||||
if (i == 0)
|
||||
break;
|
||||
|
||||
// increment zone number
|
||||
if (z < n_zones)
|
||||
z ++;
|
||||
|
||||
// if we're to the final zone, just jump to the beginning of the plot: we're done
|
||||
if (z == n_zones)
|
||||
i = 1;
|
||||
|
||||
// else, we've got to recheck this point for the next zone
|
||||
else
|
||||
i ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// no zones available: just plot the curve without zones
|
||||
else {
|
||||
QwtPlotCurve *curve;
|
||||
curve = new QwtPlotCurve("maximal power");
|
||||
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
QPen *pen = new QPen(Qt::red);
|
||||
pen->setWidth(2.0);
|
||||
curve->setPen(*pen);
|
||||
QColor brush_color = Qt::red;
|
||||
brush_color.setAlpha(64);
|
||||
curve->setBrush(brush_color); // brush fills below the line
|
||||
curve->setData(
|
||||
time_values.data(),
|
||||
power_values,
|
||||
n_values
|
||||
);
|
||||
curve->attach(thisPlot);
|
||||
delete pen;
|
||||
allCurves.append(curve);
|
||||
}
|
||||
|
||||
|
||||
// set the x-axis to span the time of the all-time curve, starting at 1 second
|
||||
thisPlot->setAxisScale(
|
||||
thisPlot->xBottom,
|
||||
1.0 / 60,
|
||||
time_values[n_values - 1]
|
||||
);
|
||||
|
||||
// set the y-axis to go from zero to the maximum power, rounded up to nearest 100 watts
|
||||
thisPlot->setAxisScale(
|
||||
thisPlot->yLeft,
|
||||
0,
|
||||
100 * ceil( power_values[0] / 100 )
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
CpintPlot::calculate(RideItem *rideItem)
|
||||
{
|
||||
QString fileName = rideItem->fileName;
|
||||
QDateTime dateTime = rideItem->dateTime;
|
||||
QDir dir(path);
|
||||
QFileInfo file(fileName);
|
||||
zones = rideItem->zones;
|
||||
|
||||
if (needToScanRides) {
|
||||
bests.clear();
|
||||
bestDates.clear();
|
||||
cpiDataInBests.clear();
|
||||
if (CPCurve) {
|
||||
CPCurve->detach();
|
||||
CPCurve = NULL;
|
||||
}
|
||||
|
||||
fflush(stderr);
|
||||
bool aborted = false;
|
||||
QList<cpi_file_info> to_update;
|
||||
cpi_files_to_update(dir, to_update);
|
||||
double progress_max = 0.0;
|
||||
if (!to_update.empty()) {
|
||||
QListIterator<cpi_file_info> i(to_update);
|
||||
while (i.hasNext()) {
|
||||
const cpi_file_info &info = i.next();
|
||||
QFile file(info.inname);
|
||||
QStringList errors;
|
||||
RideFile *rideFile =
|
||||
RideFileFactory::instance().openRideFile(file, errors);
|
||||
if (rideFile) {
|
||||
double x = rideFile->dataPoints().size();
|
||||
progress_max += x * (x + 1.0) / 2.0;
|
||||
delete rideFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
progress = new QProgressDialog(
|
||||
QString(tr("Computing critical power intervals.\n"
|
||||
"This may take a while.\n")),
|
||||
tr("Abort"), 0, 1000, this);
|
||||
double progress_sum = 0.0;
|
||||
int endingOffset = progress->labelText().size();
|
||||
if (!to_update.empty()) {
|
||||
QListIterator<cpi_file_info> i(to_update);
|
||||
int count = 1;
|
||||
while (i.hasNext()) {
|
||||
const cpi_file_info &info = i.next();
|
||||
QString existing = progress->labelText();
|
||||
existing.chop(progress->labelText().size() - endingOffset);
|
||||
progress->setLabelText(
|
||||
existing + QString(tr("Processing %1...")).arg(info.file));
|
||||
progress->setValue(count++);
|
||||
update_cpi_file(&info, progress, progress_sum, progress_max);
|
||||
QCoreApplication::processEvents();
|
||||
if (progress->wasCanceled()) {
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!aborted) {
|
||||
QString existing = progress->labelText();
|
||||
existing.chop(progress->labelText().size() - endingOffset);
|
||||
QStringList filters;
|
||||
filters << "*.cpi";
|
||||
QStringList list = dir.entryList(filters, QDir::Files, QDir::Name);
|
||||
progress->setLabelText(
|
||||
existing + tr("Aggregating over all files."));
|
||||
progress->setRange(0, list.size());
|
||||
progress->setValue(0);
|
||||
progress->show();
|
||||
QListIterator<QString> i(list);
|
||||
while (i.hasNext()) {
|
||||
const QString &filename = i.next();
|
||||
QString path = dir.absoluteFilePath(filename);
|
||||
read_one(path.toAscii().constData(), bests, bestDates, &cpiDataInBests);
|
||||
progress->setValue(progress->value() + 1);
|
||||
QCoreApplication::processEvents();
|
||||
if (progress->wasCanceled()) {
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!aborted && bests.size()) {
|
||||
int maxNonZero = 0;
|
||||
|
||||
// check that total work doesn't decrease with time
|
||||
double maxwork = 0.0;
|
||||
|
||||
for (int i = 0; i < bests.size(); ++i) {
|
||||
// record the date associated with each point's CPI file,
|
||||
if (bests[i] > 0)
|
||||
maxNonZero = i;
|
||||
|
||||
// note index is being used here in lieu of time, as the index
|
||||
// is assumed to be proportional to time
|
||||
double work = bests[i] * i;
|
||||
if ((i > 0) && (maxwork > work)) {
|
||||
bests[i] = round(maxwork / i);
|
||||
bestDates[i] = bestDates[i - 1];
|
||||
}
|
||||
else
|
||||
maxwork = work;
|
||||
}
|
||||
|
||||
// derive CP model
|
||||
if (bests.size() > 1) {
|
||||
// cp model parameters
|
||||
cp = 0;
|
||||
tau = 0;
|
||||
t0 = 0;
|
||||
|
||||
// calculate CP model from all-time best data
|
||||
deriveCPParameters();
|
||||
plot_CP_curve(this, cp, tau, t0);
|
||||
|
||||
plot_allCurve(this, maxNonZero - 1, bests.constData() + 1);
|
||||
}
|
||||
needToScanRides = false;
|
||||
}
|
||||
delete progress;
|
||||
progress = NULL;
|
||||
}
|
||||
|
||||
if (!needToScanRides) {
|
||||
if (thisCurve)
|
||||
delete thisCurve;
|
||||
thisCurve = NULL;
|
||||
QVector<double> bests;
|
||||
QVector<QDate> bestDates;
|
||||
if ((read_cpi_file(dir, file, bests, bestDates, NULL) == 0) && bests.size()) {
|
||||
double *timeArray = new double[bests.size()];
|
||||
int maxNonZero = 0;
|
||||
for (int i = 0; i < bests.size(); ++i) {
|
||||
timeArray[i] = i / 60.0;
|
||||
if (bests[i] > 0) maxNonZero = i;
|
||||
}
|
||||
if (maxNonZero > 1) {
|
||||
thisCurve = new QwtPlotCurve(
|
||||
dateTime.toString("ddd MMM d, yyyy h:mm AP"));
|
||||
thisCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
QPen *pen = new QPen(Qt::black);
|
||||
pen->setWidth(2.0);
|
||||
thisCurve->setPen(QPen(Qt::black));
|
||||
thisCurve->attach(this);
|
||||
thisCurve->setData(timeArray + 1, bests.constData() + 1,
|
||||
maxNonZero - 1);
|
||||
}
|
||||
delete [] timeArray;
|
||||
}
|
||||
}
|
||||
|
||||
replot();
|
||||
}
|
||||
|
||||
// delete a CPI file
|
||||
bool
|
||||
CpintPlot::deleteCpiFile(QString filename)
|
||||
{
|
||||
// first, get ride of the file
|
||||
if (! QFile::remove(filename))
|
||||
return false;
|
||||
|
||||
// now check to see if this file contributed to the bests
|
||||
// in the current implementation a false means it does
|
||||
// not contribute, but a true only means it at one time
|
||||
// contributed (may not in the end).
|
||||
if (cpiDataInBests.contains(filename)) {
|
||||
if (cpiDataInBests[filename])
|
||||
needToScanRides = true;
|
||||
cpiDataInBests.remove(filename);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
CpintPlot::showGrid(int state)
|
||||
{
|
||||
assert(state != Qt::PartiallyChecked);
|
||||
grid->setVisible(state == Qt::Checked);
|
||||
replot();
|
||||
}
|
||||
93
src/CpintPlot.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_CpintPlot_h
|
||||
#define _GC_CpintPlot_h 1
|
||||
|
||||
#include "Zones.h"
|
||||
#include <qwt_plot.h>
|
||||
#include <qwt_plot_marker.h> // added djconnel 06Apr2009
|
||||
#include <QtGui>
|
||||
#include <QHash>
|
||||
|
||||
class QwtPlotCurve;
|
||||
class QwtPlotGrid;
|
||||
class RideItem;
|
||||
|
||||
#define USE_T0_IN_CP_MODEL 0 // added djconnel 08Apr2009: allow 3-parameter CP model
|
||||
|
||||
bool is_ride_filename(const QString filename);
|
||||
QString ride_filename_to_cpi_filename(const QString filename);
|
||||
QDate cpi_filename_to_date(const QString filename);
|
||||
|
||||
class CpintPlot : public QwtPlot
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
CpintPlot(QString path);
|
||||
QProgressDialog *progress;
|
||||
bool needToScanRides;
|
||||
|
||||
const QwtPlotCurve *getThisCurve() const { return thisCurve; }
|
||||
|
||||
QVector<QDate> getBestDates() { return bestDates; }
|
||||
QVector<double> getBests() { return bests; }
|
||||
double cp, tau, t0; // CP model parameters
|
||||
void deriveCPParameters(); // derive the CP model parameters
|
||||
bool deleteCpiFile(QString filename); // delete a CPI file and clean up
|
||||
|
||||
|
||||
public slots:
|
||||
|
||||
void showGrid(int state);
|
||||
void calculate(RideItem *rideItem);
|
||||
void plot_CP_curve(
|
||||
CpintPlot *plot,
|
||||
double cp,
|
||||
double tau,
|
||||
double t0n
|
||||
);
|
||||
void plot_allCurve(
|
||||
CpintPlot *plot,
|
||||
int n_values,
|
||||
const double *power_values
|
||||
);
|
||||
|
||||
protected:
|
||||
|
||||
QString path;
|
||||
QwtPlotCurve *thisCurve;
|
||||
QwtPlotCurve *CPCurve;
|
||||
QList <QwtPlotCurve *> allCurves;
|
||||
QList <QwtPlotMarker *> allZoneLabels;
|
||||
void clear_CP_Curves();
|
||||
|
||||
QwtPlotGrid *grid;
|
||||
|
||||
QVector<double> bests;
|
||||
QVector<QDate> bestDates;
|
||||
|
||||
Zones **zones; // pointer to power zones added djconnel 24Apr2009
|
||||
|
||||
QHash <QString, bool> cpiDataInBests; // hash: keys are CPI files contributing to bests (at least originally)
|
||||
};
|
||||
|
||||
#endif // _GC_CpintPlot_h
|
||||
|
||||
248
src/CsvRideFile.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net),
|
||||
* Justin F. Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
|
||||
#include "CsvRideFile.h"
|
||||
#include <QRegExp>
|
||||
#include <QTextStream>
|
||||
#include <algorithm> // for std::sort
|
||||
#include <assert.h>
|
||||
#include "math.h"
|
||||
|
||||
#define MILES_TO_KM 1.609344
|
||||
|
||||
static int csvFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader("csv", new CsvFileReader());
|
||||
|
||||
RideFile *CsvFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive);
|
||||
QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive);
|
||||
bool metric;
|
||||
|
||||
|
||||
// TODO: a more robust regex for ergomo files
|
||||
// i don't have an example with english headers
|
||||
// the ergomo CSV has two rows of headers. units are on the second row.
|
||||
/*
|
||||
ZEIT,STRECKE,POWER,RPM,SPEED,PULS,HÖHE,TEMP,INTERVAL,PAUSE
|
||||
L_SEC,KM,WATT,RPM,KM/H,BPM,METER,°C,NUM,SEC
|
||||
*/
|
||||
QRegExp ergomoCSV("(ZEIT|STRECKE)", Qt::CaseInsensitive);
|
||||
bool ergomo = false;
|
||||
int unitsHeader = 1;
|
||||
int total_pause = 0;
|
||||
int currentInterval = 0;
|
||||
int prevInterval = 0;
|
||||
|
||||
// TODO: with all these formats, should the logic change to a switch/case structure?
|
||||
// The iBike format CSV file has five lines of headers (data begins on line 6)
|
||||
// starting with:
|
||||
/*
|
||||
iBike,8,english
|
||||
2008,8,8,6,32,52
|
||||
|
||||
{Various configuration data, recording interval at line[4][4]}
|
||||
Speed (mph),Wind Speed (mph),Power (W),Distance (miles),Cadence (RPM),Heartrate (BPM),Elevation (feet),Hill slope (%),Internal,Internal,Internal,DFPM Power,Latitude,Longitude
|
||||
*/
|
||||
// Modified the regExp string to allow for 2-digit version numbers - 23 Mar 2009, thm
|
||||
QRegExp iBikeCSV("iBike,\\d\\d?,[a-z]+", Qt::CaseInsensitive);
|
||||
bool iBike = false;
|
||||
int recInterval;
|
||||
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
errors << ("Could not open ride file: \""
|
||||
+ file.fileName() + "\"");
|
||||
return NULL;
|
||||
}
|
||||
int lineno = 1;
|
||||
QTextStream is(&file);
|
||||
RideFile *rideFile = new RideFile();
|
||||
while (!is.atEnd()) {
|
||||
// the readLine() method doesn't handle old Macintosh CR line endings
|
||||
// this workaround will load the the entire file if it has CR endings
|
||||
// then split and loop through each line
|
||||
// otherwise, there will be nothing to split and it will read each line as expected.
|
||||
QString linesIn = is.readLine();
|
||||
QStringList lines = linesIn.split('\r');
|
||||
// workaround for empty lines
|
||||
if(lines.isEmpty()) {
|
||||
lineno++;
|
||||
continue;
|
||||
}
|
||||
for (int li = 0; li < lines.size(); ++li) {
|
||||
QString line = lines[li];
|
||||
|
||||
if (lineno == 1) {
|
||||
if (ergomoCSV.indexIn(line) != -1) {
|
||||
ergomo = true;
|
||||
rideFile->setDeviceType("Ergomo CSV");
|
||||
unitsHeader = 2;
|
||||
++lineno;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
if(iBikeCSV.indexIn(line) != -1) {
|
||||
iBike = true;
|
||||
rideFile->setDeviceType("iBike CSV");
|
||||
unitsHeader = 5;
|
||||
++lineno;
|
||||
continue;
|
||||
}
|
||||
rideFile->setDeviceType("PowerTap CSV");
|
||||
}
|
||||
}
|
||||
if (iBike && lineno == 4) {
|
||||
// this is the line with the iBike configuration data
|
||||
// recording interval is in the [4] location (zero-based array)
|
||||
// the trailing zeroes in the configuration area seem to be causing an error
|
||||
// the number is in the format 5.000000
|
||||
recInterval = (int)line.section(',',4,4).toDouble();
|
||||
}
|
||||
if (lineno == unitsHeader) {
|
||||
if (metricUnits.indexIn(line) != -1)
|
||||
metric = true;
|
||||
else if (englishUnits.indexIn(line) != -1)
|
||||
metric = false;
|
||||
else {
|
||||
errors << "Can't find units in first line: \"" + line + "\" of file \"" + file.fileName() + "\".";
|
||||
delete rideFile;
|
||||
file.close();
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
else if (lineno > unitsHeader) {
|
||||
double minutes,nm,kph,watts,km,cad,alt,hr;
|
||||
int interval;
|
||||
int pause;
|
||||
if (!ergomo && !iBike) {
|
||||
minutes = line.section(',', 0, 0).toDouble();
|
||||
nm = line.section(',', 1, 1).toDouble();
|
||||
kph = line.section(',', 2, 2).toDouble();
|
||||
watts = line.section(',', 3, 3).toDouble();
|
||||
km = line.section(',', 4, 4).toDouble();
|
||||
cad = line.section(',', 5, 5).toDouble();
|
||||
hr = line.section(',', 6, 6).toDouble();
|
||||
interval = line.section(',', 7, 7).toInt();
|
||||
alt = line.section(',', 8, 8).toDouble();
|
||||
if (!metric) {
|
||||
km *= MILES_TO_KM;
|
||||
kph *= MILES_TO_KM;
|
||||
}
|
||||
}
|
||||
else if (iBike) {
|
||||
// this must be iBike
|
||||
// can't find time as a column.
|
||||
// will we have to extrapolate based on the recording interval?
|
||||
// reading recording interval from config data in ibike csv file
|
||||
minutes = (recInterval * lineno - unitsHeader)/60.0;
|
||||
nm = NULL; //no torque
|
||||
kph = line.section(',', 0, 0).toDouble();
|
||||
watts = line.section(',', 2, 2).toDouble();
|
||||
km = line.section(',', 3, 3).toDouble();
|
||||
cad = line.section(',', 4, 4).toDouble();
|
||||
hr = line.section(',', 5, 5).toDouble();
|
||||
interval = NULL; //not provided?
|
||||
if (!metric) {
|
||||
km *= MILES_TO_KM;
|
||||
kph *= MILES_TO_KM;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// for ergomo formatted CSV files
|
||||
minutes = line.section(',', 0, 0).toDouble() + total_pause;
|
||||
km = line.section(',', 1, 1).toDouble();
|
||||
watts = line.section(',', 2, 2).toDouble();
|
||||
cad = line.section(',', 3, 3).toDouble();
|
||||
kph = line.section(',', 4, 4).toDouble();
|
||||
hr = line.section(',', 5, 5).toDouble();
|
||||
alt = line.section(',', 6, 6).toDouble();
|
||||
interval = line.section(',', 8, 8).toInt();
|
||||
if (interval != prevInterval) {
|
||||
prevInterval = interval;
|
||||
if (interval != 0) currentInterval++;
|
||||
}
|
||||
if (interval != 0) interval = currentInterval;
|
||||
pause = line.section(',', 9, 9).toInt();
|
||||
total_pause += pause;
|
||||
nm = NULL; // torque is not provided in the Ergomo file
|
||||
|
||||
// the ergomo records the time in whole seconds
|
||||
// RECORDING INT. 1, 2, 5, 10, 15 or 30 per sec
|
||||
// Time is *always* perfectly sequential. To find pauses,
|
||||
// you need to read the PAUSE column.
|
||||
minutes = minutes/60.0;
|
||||
|
||||
if (!metric) {
|
||||
km *= MILES_TO_KM;
|
||||
kph *= MILES_TO_KM;
|
||||
}
|
||||
}
|
||||
|
||||
// PT reports no data as watts == -1.
|
||||
if (watts == -1)
|
||||
watts = 0;
|
||||
|
||||
rideFile->appendPoint(minutes * 60.0, cad, hr, km,
|
||||
kph, nm, watts, alt, interval);
|
||||
}
|
||||
++lineno;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
|
||||
// To estimate the recording interval, take the median of the
|
||||
// first 1000 samples and round to nearest millisecond.
|
||||
int n = rideFile->dataPoints().size();
|
||||
n = qMin(n, 1000);
|
||||
if (n >= 2) {
|
||||
double *secs = new double[n-1];
|
||||
for (int i = 0; i < n-1; ++i) {
|
||||
double now = rideFile->dataPoints()[i]->secs;
|
||||
double then = rideFile->dataPoints()[i+1]->secs;
|
||||
secs[i] = then - now;
|
||||
}
|
||||
std::sort(secs, secs + n - 1);
|
||||
int mid = n / 2 - 1;
|
||||
double recint = round(secs[mid] * 1000.0) / 1000.0;
|
||||
rideFile->setRecIntSecs(recint);
|
||||
}
|
||||
// less than 2 data points is not a valid ride file
|
||||
else {
|
||||
errors << "Insufficient valid data in file \"" + file.fileName() + "\".";
|
||||
delete rideFile;
|
||||
file.close();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
|
||||
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$");
|
||||
if (rideTime.indexIn(file.fileName()) >= 0) {
|
||||
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
|
||||
rideTime.cap(2).toInt(),
|
||||
rideTime.cap(3).toInt()),
|
||||
QTime(rideTime.cap(4).toInt(),
|
||||
rideTime.cap(5).toInt(),
|
||||
rideTime.cap(6).toInt()));
|
||||
rideFile->setStartTime(datetime);
|
||||
}
|
||||
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
29
src/CsvRideFile.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _CsvRideFile_h
|
||||
#define _CsvRideFile_h
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
struct CsvFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
};
|
||||
|
||||
#endif // _CsvRideFile_h
|
||||
|
||||
231
src/D2XX.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "D2XX.h"
|
||||
#include <dlfcn.h>
|
||||
|
||||
// D2XXWrapper is a wrapper around libftd2xx to make it amenable to loading
|
||||
// with dlopen().
|
||||
|
||||
#define LOAD_SYM(type,var,name) \
|
||||
var = (type*) dlsym(handle, name); \
|
||||
if (!var) { \
|
||||
error = QString("could not load symbol ") + name; \
|
||||
return false; \
|
||||
}
|
||||
#ifdef WIN32
|
||||
#define WIN32_STDCALL __stdcall
|
||||
#else
|
||||
#define WIN32_STDCALL
|
||||
#endif
|
||||
|
||||
typedef FT_STATUS WIN32_STDCALL FP_OpenEx(PVOID pArg1, DWORD Flags, FT_HANDLE *pHandle);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_Close(FT_HANDLE ftHandle);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_SetBaudRate(FT_HANDLE ftHandle, ULONG BaudRate);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_SetDataCharacteristics(FT_HANDLE ftHandle, UCHAR WordLength, UCHAR StopBits, UCHAR Parity);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_SetFlowControl(FT_HANDLE ftHandle, USHORT FlowControl, UCHAR XonChar, UCHAR XoffChar);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_GetQueueStatus(FT_HANDLE ftHandle, DWORD *dwRxBytes);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_SetTimeouts(FT_HANDLE ftHandle, ULONG ReadTimeout, ULONG WriteTimeout);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_Read(FT_HANDLE ftHandle, LPVOID lpBuffer, DWORD nBufferSize, LPDWORD lpBytesReturned);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_Write(FT_HANDLE ftHandle, LPVOID lpBuffer, DWORD nBufferSize, LPDWORD lpBytesWritten);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_CreateDeviceInfoList(LPDWORD lpdwNumDevs);
|
||||
typedef FT_STATUS WIN32_STDCALL FP_GetDeviceInfoList(FT_DEVICE_LIST_INFO_NODE *pDest, LPDWORD lpdwNumDevs);
|
||||
|
||||
struct D2XXWrapper {
|
||||
void *handle;
|
||||
FP_OpenEx *open_ex;
|
||||
FP_Close *close;
|
||||
FP_SetBaudRate *set_baud_rate;
|
||||
FP_SetDataCharacteristics *set_data_characteristics;
|
||||
FP_SetFlowControl *set_flow_control;
|
||||
FP_GetQueueStatus *get_queue_status;
|
||||
FP_SetTimeouts *set_timeouts;
|
||||
FP_Read *read;
|
||||
FP_Write *write;
|
||||
FP_CreateDeviceInfoList *create_device_info_list;
|
||||
FP_GetDeviceInfoList *get_device_info_list;
|
||||
D2XXWrapper() : handle(NULL) {}
|
||||
~D2XXWrapper() { if (handle) dlclose(handle); }
|
||||
bool init(QString &error)
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
const char *libname = "libftd2xx.so";
|
||||
#elif defined(Q_OS_WIN32)
|
||||
const char *libname = "ftd2xx.dll";
|
||||
#elif defined(Q_OS_DARWIN)
|
||||
const char *libname = "libftd2xx.dylib";
|
||||
#endif
|
||||
handle = dlopen(libname, RTLD_NOW);
|
||||
if (!handle) {
|
||||
error = QString("Couldn't load library ") + libname + ".";
|
||||
return false;
|
||||
}
|
||||
LOAD_SYM(FP_OpenEx, open_ex, "FT_OpenEx");
|
||||
LOAD_SYM(FP_Close, close, "FT_Close");
|
||||
LOAD_SYM(FP_SetBaudRate, set_baud_rate, "FT_SetBaudRate");
|
||||
LOAD_SYM(FP_SetDataCharacteristics, set_data_characteristics, "FT_SetDataCharacteristics");
|
||||
LOAD_SYM(FP_SetFlowControl, set_flow_control, "FT_SetFlowControl");
|
||||
LOAD_SYM(FP_GetQueueStatus, get_queue_status, "FT_GetQueueStatus");
|
||||
LOAD_SYM(FP_SetTimeouts, set_timeouts, "FT_SetTimeouts");
|
||||
LOAD_SYM(FP_Read, read, "FT_Read");
|
||||
LOAD_SYM(FP_Write, write, "FT_Write");
|
||||
LOAD_SYM(FP_CreateDeviceInfoList, create_device_info_list, "FT_CreateDeviceInfoList");
|
||||
LOAD_SYM(FP_GetDeviceInfoList, get_device_info_list, "FT_GetDeviceInfoList");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static D2XXWrapper *lib; // singleton lib instance
|
||||
|
||||
bool D2XXRegistered = CommPort::addListFunction(&D2XX::myListCommPorts);
|
||||
|
||||
D2XX::D2XX(const FT_DEVICE_LIST_INFO_NODE &info) :
|
||||
info(info), isOpen(false)
|
||||
{
|
||||
}
|
||||
|
||||
D2XX::~D2XX()
|
||||
{
|
||||
if (isOpen)
|
||||
close();
|
||||
}
|
||||
|
||||
bool
|
||||
D2XX::open(QString &err)
|
||||
{
|
||||
assert(!isOpen);
|
||||
FT_STATUS ftStatus =
|
||||
lib->open_ex(info.Description, FT_OPEN_BY_DESCRIPTION, &ftHandle);
|
||||
if (ftStatus != FT_OK) {
|
||||
err = QString("FT_Open: %1").arg(ftStatus);
|
||||
return false;
|
||||
}
|
||||
isOpen = true;
|
||||
ftStatus = lib->set_baud_rate(ftHandle, 9600);
|
||||
if (ftStatus != FT_OK) {
|
||||
err = QString("FT_SetBaudRate: %1").arg(ftStatus);
|
||||
close();
|
||||
}
|
||||
|
||||
ftStatus = lib->set_data_characteristics(ftHandle,FT_BITS_8,FT_STOP_BITS_1,
|
||||
FT_PARITY_NONE);
|
||||
if (ftStatus != FT_OK) {
|
||||
err = QString("FT_SetDataCharacteristics: %1").arg(ftStatus);
|
||||
close();
|
||||
}
|
||||
|
||||
ftStatus = lib->set_flow_control(ftHandle,FT_FLOW_NONE,
|
||||
'0','0'); //the 0's are ignored
|
||||
if (ftStatus != FT_OK) {
|
||||
err = QString("FT_SetFlowControl: %1").arg(ftStatus);
|
||||
close();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
D2XX::close()
|
||||
{
|
||||
assert(isOpen);
|
||||
lib->close(ftHandle);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
int
|
||||
D2XX::read(void *buf, size_t nbyte, QString &err)
|
||||
{
|
||||
assert(isOpen);
|
||||
DWORD rxbytes;
|
||||
FT_STATUS ftStatus = lib->get_queue_status(ftHandle, &rxbytes);
|
||||
if (ftStatus != FT_OK) {
|
||||
err = QString("FT_GetQueueStatus: %1").arg(ftStatus);
|
||||
return -1;
|
||||
}
|
||||
// printf("rxbytes=%d\n", (int) rxbytes);
|
||||
// Return immediately whenever there's something to read.
|
||||
if (rxbytes > 0 && rxbytes < nbyte)
|
||||
nbyte = rxbytes;
|
||||
if (nbyte > rxbytes)
|
||||
lib->set_timeouts(ftHandle, 5000, 5000);
|
||||
DWORD n;
|
||||
ftStatus = lib->read(ftHandle, buf, nbyte, &n);
|
||||
if (ftStatus == FT_OK)
|
||||
return n;
|
||||
err = QString("FT_Read: %1").arg(ftStatus);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
D2XX::write(void *buf, size_t nbyte, QString &err)
|
||||
{
|
||||
assert(isOpen);
|
||||
DWORD n;
|
||||
FT_STATUS ftStatus = lib->write(ftHandle, buf, nbyte, &n);
|
||||
if (ftStatus == FT_OK)
|
||||
return n;
|
||||
err = QString("FT_Write: %1").arg(ftStatus);
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString
|
||||
D2XX::name() const
|
||||
{
|
||||
return QString("D2XX: ") + info.Description;
|
||||
}
|
||||
|
||||
QVector<CommPortPtr>
|
||||
D2XX::myListCommPorts(QString &err)
|
||||
{
|
||||
QVector<CommPortPtr> result;
|
||||
if (!lib) {
|
||||
lib = new D2XXWrapper;
|
||||
if (!lib->init(err)) {
|
||||
delete lib;
|
||||
lib = NULL;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
DWORD numDevs;
|
||||
FT_STATUS ftStatus = lib->create_device_info_list(&numDevs);
|
||||
if(ftStatus != FT_OK) {
|
||||
err = QString("FT_CreateDeviceInfoList: %1").arg(ftStatus);
|
||||
return result;
|
||||
}
|
||||
FT_DEVICE_LIST_INFO_NODE *devInfo = new FT_DEVICE_LIST_INFO_NODE[numDevs];
|
||||
ftStatus = lib->get_device_info_list(devInfo, &numDevs);
|
||||
if (ftStatus != FT_OK)
|
||||
err = QString("FT_GetDeviceInfoList: %1").arg(ftStatus);
|
||||
else {
|
||||
for (DWORD i = 0; i < numDevs; i++)
|
||||
result.append(CommPortPtr(new D2XX(devInfo[i])));
|
||||
}
|
||||
delete [] devInfo;
|
||||
// If we can't open a D2XX device, it's usually because the VCP drivers
|
||||
// are installed, so it should also show up in the list of serial devices.
|
||||
for (int i = 0; i < result.size(); ++i) {
|
||||
CommPortPtr dev = result[i];
|
||||
QString tmp;
|
||||
if (dev->open(tmp))
|
||||
dev->close();
|
||||
else
|
||||
result.remove(i--);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
51
src/D2XX.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_PT_D2XX_h
|
||||
#define _GC_PT_D2XX_h 1
|
||||
|
||||
#include "CommPort.h"
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <ftd2xx.h>
|
||||
|
||||
class D2XX : public CommPort
|
||||
{
|
||||
D2XX(const D2XX &);
|
||||
D2XX& operator=(const D2XX &);
|
||||
|
||||
FT_DEVICE_LIST_INFO_NODE info;
|
||||
FT_HANDLE ftHandle;
|
||||
bool isOpen;
|
||||
D2XX(const FT_DEVICE_LIST_INFO_NODE &info);
|
||||
|
||||
public:
|
||||
|
||||
static QVector<CommPortPtr> myListCommPorts(QString &err);
|
||||
|
||||
virtual ~D2XX();
|
||||
virtual bool open(QString &err);
|
||||
virtual void close();
|
||||
virtual int read(void *buf, size_t nbyte, QString &err);
|
||||
virtual int write(void *buf, size_t nbyte, QString &err);
|
||||
virtual QString name() const;
|
||||
};
|
||||
|
||||
#endif // _GC_PT_D2XX_h
|
||||
|
||||
284
src/DBAccess.cpp
Normal file
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Justin Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DBAccess.h"
|
||||
#include <QtSql>
|
||||
#include <QtGui>
|
||||
#include "RideFile.h"
|
||||
#include "Zones.h"
|
||||
#include "Settings.h"
|
||||
#include "RideItem.h"
|
||||
#include "RideMetric.h"
|
||||
#include "TimeUtils.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <QtXml/QtXml>
|
||||
#include "SummaryMetrics.h"
|
||||
|
||||
|
||||
DBAccess::DBAccess(QDir home)
|
||||
{
|
||||
initDatabase(home);
|
||||
}
|
||||
|
||||
void DBAccess::closeConnection()
|
||||
{
|
||||
db.close();
|
||||
}
|
||||
|
||||
QSqlDatabase DBAccess::initDatabase(QDir home)
|
||||
{
|
||||
|
||||
|
||||
if(db.isOpen())
|
||||
return db;
|
||||
|
||||
db = QSqlDatabase::addDatabase("QSQLITE");
|
||||
db.setDatabaseName(home.absolutePath() + "/metricDB");
|
||||
if (!db.open()) {
|
||||
QMessageBox::critical(0, qApp->tr("Cannot open database"),
|
||||
qApp->tr("Unable to establish a database connection.\n"
|
||||
"This example needs SQLite support. Please read "
|
||||
"the Qt SQL driver documentation for information how "
|
||||
"to build it.\n\n"
|
||||
"Click Cancel to exit."), QMessageBox::Cancel);
|
||||
return db;
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
bool DBAccess::createMetricsTable()
|
||||
{
|
||||
QSqlQuery query;
|
||||
bool rc = query.exec("create table metrics (id integer primary key autoincrement, "
|
||||
"filename varchar,"
|
||||
"ride_date date,"
|
||||
"ride_time double, "
|
||||
"average_cad double,"
|
||||
"workout_time double, "
|
||||
"total_distance double,"
|
||||
"x_power double,"
|
||||
"average_speed double,"
|
||||
"total_work double,"
|
||||
"average_power double,"
|
||||
"average_hr double,"
|
||||
"relative_intensity double,"
|
||||
"bike_score double)");
|
||||
|
||||
if(!rc)
|
||||
qDebug() << query.lastError();
|
||||
|
||||
return rc;
|
||||
|
||||
}
|
||||
|
||||
bool DBAccess::createSeasonsTable()
|
||||
{
|
||||
QSqlQuery query;
|
||||
bool rc = query.exec("CREATE TABLE seasons(id integer primary key autoincrement,"
|
||||
"start_date date,"
|
||||
"end_date date,"
|
||||
"name varchar)");
|
||||
|
||||
if(!rc)
|
||||
qDebug() << query.lastError();
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
bool DBAccess::createDatabase()
|
||||
{
|
||||
|
||||
bool rc = false;
|
||||
|
||||
rc = createMetricsTable();
|
||||
|
||||
if(!rc)
|
||||
return rc;
|
||||
|
||||
rc = createIndex();
|
||||
if(!rc)
|
||||
return rc;
|
||||
|
||||
//Check to see if the table already exists..
|
||||
QStringList tableList = db.tables(QSql::Tables);
|
||||
if(!tableList.contains("seasons"))
|
||||
return createSeasonsTable();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool DBAccess::createIndex()
|
||||
{
|
||||
QSqlQuery query;
|
||||
query.prepare("create INDEX IDX_FILENAME on metrics(filename)");
|
||||
bool rc = query.exec();
|
||||
if(!rc)
|
||||
qDebug() << query.lastError();
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
bool DBAccess::importRide(SummaryMetrics *summaryMetrics )
|
||||
{
|
||||
|
||||
QSqlQuery query;
|
||||
|
||||
query.prepare("insert into metrics (filename, ride_date, ride_time, average_cad, workout_time, total_distance,"
|
||||
"x_power, average_speed, total_work, average_power, average_hr,"
|
||||
"relative_intensity, bike_score) values (?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
|
||||
|
||||
query.addBindValue(summaryMetrics->getFileName());
|
||||
query.addBindValue(summaryMetrics->getRideDate());
|
||||
query.addBindValue(summaryMetrics->getRideTime());
|
||||
query.addBindValue(summaryMetrics->getCadence());
|
||||
query.addBindValue(summaryMetrics->getWorkoutTime());
|
||||
query.addBindValue(summaryMetrics->getDistance());
|
||||
query.addBindValue(summaryMetrics->getXPower());
|
||||
query.addBindValue(summaryMetrics->getSpeed());
|
||||
query.addBindValue(summaryMetrics->getTotalWork());
|
||||
query.addBindValue(summaryMetrics->getWatts());
|
||||
query.addBindValue(summaryMetrics->getHeartRate());
|
||||
query.addBindValue(summaryMetrics->getRelativeIntensity());
|
||||
query.addBindValue(summaryMetrics->getBikeScore());
|
||||
|
||||
bool rc = query.exec();
|
||||
|
||||
if(!rc)
|
||||
{
|
||||
qDebug() << query.lastError();
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
QStringList DBAccess::getAllFileNames()
|
||||
{
|
||||
QSqlQuery query("SELECT filename from metrics");
|
||||
QStringList fileList;
|
||||
|
||||
while(query.next())
|
||||
{
|
||||
QString filename = query.value(0).toString();
|
||||
fileList << filename;
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
QList<QDateTime> DBAccess::getAllDates()
|
||||
{
|
||||
QSqlQuery query("SELECT ride_date from metrics");
|
||||
QList<QDateTime> dates;
|
||||
|
||||
while(query.next())
|
||||
{
|
||||
QDateTime date = query.value(0).toDateTime();
|
||||
dates << date;
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
QList<SummaryMetrics> DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
|
||||
{
|
||||
QList<SummaryMetrics> metrics;
|
||||
|
||||
|
||||
QSqlQuery query("SELECT filename, ride_date, ride_time, average_cad, workout_time, total_distance,"
|
||||
"x_power, average_speed, total_work, average_power, average_hr,"
|
||||
"relative_intensity, bike_scoreFROM metrics WHERE ride_date >=:start AND ride_date <=:end");
|
||||
query.bindValue(":start", start);
|
||||
query.bindValue(":end", end);
|
||||
|
||||
while(query.next())
|
||||
{
|
||||
|
||||
SummaryMetrics summaryMetrics;
|
||||
summaryMetrics.setFileName(query.value(0).toString());
|
||||
summaryMetrics.setRideDate(query.value(1).toDateTime());
|
||||
summaryMetrics.setRideTime(query.value(2).toDouble());
|
||||
summaryMetrics.setCadence(query.value(3).toDouble());
|
||||
summaryMetrics.setWorkoutTime(query.value(4).toDouble());
|
||||
summaryMetrics.setDistance(query.value(5).toDouble());
|
||||
summaryMetrics.setXPower(query.value(6).toDouble());
|
||||
summaryMetrics.setSpeed(query.value(7).toDouble());
|
||||
summaryMetrics.setTotalWork(query.value(8).toDouble());
|
||||
summaryMetrics.setWatts(query.value(9).toDouble());
|
||||
summaryMetrics.setHeartRate(query.value(10).toDouble());
|
||||
summaryMetrics.setRelativeIntensity(query.value(11).toDouble());
|
||||
summaryMetrics.setBikeScore(query.value(12).toDouble());
|
||||
|
||||
metrics << summaryMetrics;
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool DBAccess::createSeason(Season season)
|
||||
{
|
||||
QSqlQuery query;
|
||||
|
||||
query.prepare("INSERT INTO season (start_date, end_date, name) values (?,?,?)");
|
||||
|
||||
|
||||
query.addBindValue(season.getStart());
|
||||
query.addBindValue(season.getEnd());
|
||||
query.addBindValue(season.getName());
|
||||
|
||||
bool rc = query.exec();
|
||||
|
||||
if(!rc)
|
||||
qDebug() << query.lastError();
|
||||
|
||||
return rc;
|
||||
|
||||
}
|
||||
|
||||
QList<Season> DBAccess::getAllSeasons()
|
||||
{
|
||||
QSqlQuery query("SELECT start_date, end_date, name from season");
|
||||
QList<Season> seasons;
|
||||
|
||||
while(query.next())
|
||||
{
|
||||
Season season;
|
||||
season.setStart(query.value(0).toDateTime());
|
||||
season.setEnd(query.value(1).toDateTime());
|
||||
season.setName(query.value(2).toString());
|
||||
seasons << season;
|
||||
|
||||
}
|
||||
return seasons;
|
||||
|
||||
}
|
||||
|
||||
bool DBAccess::dropMetricTable()
|
||||
{
|
||||
|
||||
|
||||
QStringList tableList = db.tables(QSql::Tables);
|
||||
if(!tableList.contains("metrics"))
|
||||
return true;
|
||||
|
||||
QSqlQuery query("DROP TABLE metrics");
|
||||
|
||||
return query.exec();
|
||||
}
|
||||
61
src/DBAccess.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_DBAccess_h
|
||||
#define _GC_DBAccess_h 1
|
||||
|
||||
|
||||
|
||||
#import <QDir>
|
||||
#import <QHash>
|
||||
#import <QtSql>
|
||||
#import "SummaryMetrics.h"
|
||||
#import "Season.h"
|
||||
|
||||
class RideFile;
|
||||
class Zones;
|
||||
class RideMetric;
|
||||
class DBAccess
|
||||
{
|
||||
|
||||
public:
|
||||
DBAccess(QDir home);
|
||||
typedef QHash<QString,RideMetric*> MetricMap;
|
||||
void importAllRides(QDir path, Zones *zones);
|
||||
bool importRide(SummaryMetrics *summaryMetrics);
|
||||
bool createDatabase();
|
||||
QStringList getAllFileNames();
|
||||
void closeConnection();
|
||||
QList<QDateTime> getAllDates();
|
||||
QList<SummaryMetrics> getAllMetricsFor(QDateTime start, QDateTime end);
|
||||
bool createSeasonsTable();
|
||||
bool createMetricsTable();
|
||||
bool createSeason(Season season);
|
||||
QList<Season> getAllSeasons();
|
||||
bool dropMetricTable();
|
||||
//bool deleteSeason(Season season);
|
||||
|
||||
|
||||
|
||||
private:
|
||||
QSqlDatabase db;
|
||||
bool createIndex();
|
||||
QSqlDatabase initDatabase(QDir home);
|
||||
|
||||
};
|
||||
#endif
|
||||
140
src/DatePickerDialog.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Justin F. Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DatePickerDialog.h"
|
||||
#include "Settings.h"
|
||||
#include <QtGui>
|
||||
|
||||
void DatePickerDialog::setupUi(QDialog *DatePickerDialog)
|
||||
{
|
||||
if (DatePickerDialog->objectName().isEmpty())
|
||||
DatePickerDialog->setObjectName(QString::fromUtf8("DatePickerDialog"));
|
||||
DatePickerDialog->setWindowModality(Qt::WindowModal);
|
||||
DatePickerDialog->setAcceptDrops(false);
|
||||
DatePickerDialog->setModal(true);
|
||||
|
||||
QGridLayout *mainGrid = new QGridLayout(this); // a 2 x n grid
|
||||
lblOccur = new QLabel("When did this ride occur?", this);
|
||||
mainGrid->addWidget(lblOccur, 0,0);
|
||||
dateTimeEdit = new QDateTimeEdit(this);
|
||||
|
||||
// preset dialog to today's date -thm
|
||||
QDateTime *dt = new QDateTime;
|
||||
date = dt->currentDateTime();
|
||||
dateTimeEdit->setDateTime(date);
|
||||
|
||||
mainGrid->addWidget(dateTimeEdit,0,1);
|
||||
lblBrowse = new QLabel("Choose a CSV file to upload", this);
|
||||
mainGrid->addWidget(lblBrowse, 1,0);
|
||||
btnBrowse = new QPushButton(this);
|
||||
mainGrid->addWidget(btnBrowse,2,0);
|
||||
txtBrowse = new QLineEdit(this);
|
||||
mainGrid->addWidget(txtBrowse,2,1);
|
||||
|
||||
btnOK = new QPushButton(this);
|
||||
mainGrid->addWidget(btnOK, 3,0);
|
||||
btnCancel = new QPushButton(this);
|
||||
mainGrid->addWidget(btnCancel, 3,1);
|
||||
|
||||
DatePickerDialog->setWindowTitle(
|
||||
QApplication::translate("DatePickerDialog", "Import CSV file", 0,
|
||||
QApplication::UnicodeUTF8));
|
||||
|
||||
btnBrowse->setText(
|
||||
QApplication::translate("DatePickerDialog", "File to import...", 0,
|
||||
QApplication::UnicodeUTF8));
|
||||
btnOK->setText(
|
||||
QApplication::translate("DatePickerDialog", "OK", 0,
|
||||
QApplication::UnicodeUTF8));
|
||||
btnCancel->setText(
|
||||
QApplication::translate("DatePickerDialog", "Cancel", 0,
|
||||
QApplication::UnicodeUTF8));
|
||||
|
||||
connect(btnOK, SIGNAL(clicked()), this, SLOT(on_btnOK_clicked()));
|
||||
connect(btnBrowse, SIGNAL(clicked()), this, SLOT(on_btnBrowse_clicked()));
|
||||
connect(btnCancel, SIGNAL(clicked()), this, SLOT(on_btnCancel_clicked()));
|
||||
|
||||
// disable date picker and OK button until a file has been selected
|
||||
dateTimeEdit->setEnabled(FALSE);
|
||||
lblOccur->setEnabled(FALSE);
|
||||
btnOK->setEnabled(FALSE);
|
||||
|
||||
Q_UNUSED(DatePickerDialog);
|
||||
}
|
||||
|
||||
DatePickerDialog::DatePickerDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setupUi(this);
|
||||
}
|
||||
|
||||
void DatePickerDialog::on_btnOK_clicked()
|
||||
{
|
||||
canceled = false;
|
||||
date = dateTimeEdit->dateTime();
|
||||
accept();
|
||||
}
|
||||
|
||||
void DatePickerDialog::on_btnBrowse_clicked()
|
||||
{
|
||||
//First check to see if the Library folder exists where the executable is (for USB sticks)
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
|
||||
QVariant lastDirVar = settings->value(GC_SETTINGS_LAST_IMPORT_PATH);
|
||||
QString lastDir = (lastDirVar != QVariant())
|
||||
? lastDirVar.toString() : QDir::homePath();
|
||||
fileName = QFileDialog::getOpenFileName(
|
||||
this, tr("Import CSV"), lastDir,
|
||||
tr("Comma Separated Values (*.csv)"));
|
||||
if (!fileName.isEmpty()) {
|
||||
lastDir = QFileInfo(fileName).absolutePath();
|
||||
settings->setValue(GC_SETTINGS_LAST_IMPORT_PATH, lastDir);
|
||||
|
||||
// Find the datetimestamp from the filename.
|
||||
// If we can't, use the creation time.
|
||||
// eg. GoldenCheetah YYYY_MM_DD_HH_MM_SS.csv
|
||||
// Ergomo YYYYMMDD_HHMMSS_NAME_SURNAME.CSV
|
||||
QFileInfo *qfi = new QFileInfo(fileName);
|
||||
QString name = qfi->baseName();
|
||||
QRegExp rxGoldenCheetah("^(19|20)\\d\\d_[01]\\d_[0123]\\d_[012]\\d_[012345]\\d_[012345]\\d$");
|
||||
QRegExp rxErgomo("^(19|20)\\d\\d[01]\\d[0123]\\d_[012]\\d[012345]\\d[012345]\\d_[A-Z_]+$");
|
||||
if (rxGoldenCheetah.indexIn(name) == 0) {
|
||||
date = QDateTime::fromString(name.left(19), "yyyy_MM_dd_hh_mm_ss");
|
||||
} else if (rxErgomo.indexIn(name) == 0) {
|
||||
date = QDateTime::fromString(name.left(15), "yyyyMMdd_hhmmss");
|
||||
} else {
|
||||
date = qfi->created();
|
||||
}
|
||||
// and put it into the datePicker dialog
|
||||
dateTimeEdit->setDateTime(date);
|
||||
|
||||
}
|
||||
txtBrowse->setText(fileName);
|
||||
|
||||
// allow date to be changed, and enable OK button
|
||||
dateTimeEdit->setEnabled(TRUE);
|
||||
lblOccur->setEnabled(TRUE);
|
||||
btnOK->setEnabled(TRUE);
|
||||
}
|
||||
|
||||
void DatePickerDialog::on_btnCancel_clicked()
|
||||
{
|
||||
canceled = true;
|
||||
reject();
|
||||
}
|
||||
|
||||
48
src/DatePickerDialog.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Justin F. Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QtGui>
|
||||
|
||||
class DatePickerDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DatePickerDialog(QWidget *parent = 0);
|
||||
QString fileName;
|
||||
QDateTime date;
|
||||
bool canceled;
|
||||
QString currentText;
|
||||
QLabel *lblOccur;
|
||||
QDateTimeEdit *dateTimeEdit;
|
||||
QHBoxLayout *hboxLayout1;
|
||||
QLabel *lblImport;
|
||||
QLabel *lblBrowse;
|
||||
QPushButton *btnBrowse;
|
||||
QLineEdit *txtBrowse;
|
||||
QPushButton *btnOK;
|
||||
QPushButton *btnCancel;
|
||||
void setupUi(QDialog*);
|
||||
|
||||
private slots:
|
||||
void on_btnOK_clicked();
|
||||
void on_btnBrowse_clicked();
|
||||
void on_btnCancel_clicked();
|
||||
};
|
||||
|
||||
62
src/DaysScaleDraw.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Robert Carlsen (robert@robertcarlsen.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
* DayScaleDraw.h
|
||||
* GoldenCheetah
|
||||
*
|
||||
* Provides specialized formatting for Plot axes
|
||||
*/
|
||||
|
||||
#include <qwt_scale_draw.h>
|
||||
|
||||
class DaysScaleDraw: public QwtScaleDraw
|
||||
{
|
||||
public:
|
||||
DaysScaleDraw()
|
||||
{
|
||||
}
|
||||
virtual QwtText label(double v) const
|
||||
{
|
||||
switch(int(v))
|
||||
{
|
||||
case 1:
|
||||
return QString("Mon");
|
||||
break;
|
||||
case 2:
|
||||
return QString("Tue");
|
||||
break;
|
||||
case 3:
|
||||
return QString("Wed");
|
||||
break;
|
||||
case 4:
|
||||
return QString("Thu");
|
||||
break;
|
||||
case 5:
|
||||
return QString("Fri");
|
||||
break;
|
||||
case 6:
|
||||
return QString("Sat");
|
||||
break;
|
||||
case 7:
|
||||
return QString("Sun");
|
||||
break;
|
||||
default:
|
||||
return QString(int(v));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
53
src/Device.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "Device.h"
|
||||
|
||||
typedef QMap<QString,Device*> DevicesMap;
|
||||
|
||||
static DevicesMap *devicesPtr;
|
||||
|
||||
inline DevicesMap &
|
||||
devices()
|
||||
{
|
||||
if (devicesPtr == NULL)
|
||||
devicesPtr = new QMap<QString,Device*>;
|
||||
return *devicesPtr;
|
||||
}
|
||||
|
||||
QList<QString>
|
||||
Device::deviceTypes()
|
||||
{
|
||||
return devices().keys();
|
||||
}
|
||||
|
||||
Device &
|
||||
Device::device(const QString &deviceType)
|
||||
{
|
||||
assert(devices().contains(deviceType));
|
||||
return *devices().value(deviceType);
|
||||
}
|
||||
|
||||
bool
|
||||
Device::addDevice(const QString &deviceType, Device *device)
|
||||
{
|
||||
assert(!devices().contains(deviceType));
|
||||
devices().insert(deviceType, device);
|
||||
return true;
|
||||
}
|
||||
|
||||
43
src/Device.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_Device_h
|
||||
#define _GC_Device_h 1
|
||||
|
||||
#include "CommPort.h"
|
||||
#include <boost/function.hpp>
|
||||
|
||||
struct Device
|
||||
{
|
||||
virtual ~Device() {}
|
||||
|
||||
typedef boost::function<bool (const QString &statusText)> StatusCallback;
|
||||
|
||||
virtual QString downloadInstructions() const = 0;
|
||||
virtual bool download(CommPortPtr dev, const QDir &tmpdir,
|
||||
QString &tmpname, QString &filename,
|
||||
StatusCallback statusCallback, QString &err) = 0;
|
||||
virtual void cleanup(CommPortPtr dev) { (void) dev; }
|
||||
|
||||
static QList<QString> deviceTypes();
|
||||
static Device &device(const QString &deviceType);
|
||||
static bool addDevice(const QString &deviceType, Device *device);
|
||||
};
|
||||
|
||||
#endif // _GC_Device_h
|
||||
|
||||
213
src/DownloadRideDialog.cpp
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* $Id: DownloadRideDialog.cpp,v 1.4 2006/08/11 20:02:13 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006-2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DownloadRideDialog.h"
|
||||
#include "Device.h"
|
||||
#include "MainWindow.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <QtGui>
|
||||
#include <boost/bind.hpp>
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
DownloadRideDialog::DownloadRideDialog(MainWindow *mainWindow,
|
||||
const QDir &home) :
|
||||
mainWindow(mainWindow), home(home), cancelled(false),
|
||||
downloadInProgress(false)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle("Download Ride Data");
|
||||
|
||||
portCombo = new QComboBox(this);
|
||||
|
||||
QLabel *instructLabel = new QLabel(tr("Instructions:"), this);
|
||||
label = new QLabel(this);
|
||||
label->setIndent(10);
|
||||
|
||||
deviceCombo = new QComboBox(this);
|
||||
QList<QString> deviceTypes = Device::deviceTypes();
|
||||
assert(deviceTypes.size() > 0);
|
||||
BOOST_FOREACH(QString device, deviceTypes) {
|
||||
deviceCombo->addItem(device);
|
||||
}
|
||||
|
||||
downloadButton = new QPushButton(tr("&Download"), this);
|
||||
rescanButton = new QPushButton(tr("&Rescan"), this);
|
||||
cancelButton = new QPushButton(tr("&Cancel"), this);
|
||||
|
||||
connect(downloadButton, SIGNAL(clicked()), this, SLOT(downloadClicked()));
|
||||
connect(rescanButton, SIGNAL(clicked()), this, SLOT(scanCommPorts()));
|
||||
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addWidget(downloadButton);
|
||||
buttonLayout->addWidget(rescanButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addWidget(new QLabel(tr("Select port:"), this));
|
||||
mainLayout->addWidget(portCombo);
|
||||
mainLayout->addWidget(new QLabel(tr("Select device type:"), this));
|
||||
mainLayout->addWidget(deviceCombo);
|
||||
mainLayout->addWidget(instructLabel);
|
||||
mainLayout->addWidget(label);
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
scanCommPorts();
|
||||
}
|
||||
|
||||
void
|
||||
DownloadRideDialog::setReadyInstruct()
|
||||
{
|
||||
if (portCombo->count() == 0) {
|
||||
label->setText(tr("No devices found. Make sure the device\n"
|
||||
"unit is plugged into the computer,\n"
|
||||
"then click \"Rescan\" to check again."));
|
||||
downloadButton->setEnabled(false);
|
||||
}
|
||||
else {
|
||||
Device &device = Device::device(deviceCombo->currentText());
|
||||
QString inst = device.downloadInstructions();
|
||||
if (inst.size() == 0)
|
||||
label->setText("Click Download to begin downloading.");
|
||||
else
|
||||
label->setText(inst + ", \nthen click Download.");
|
||||
downloadButton->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DownloadRideDialog::scanCommPorts()
|
||||
{
|
||||
portCombo->clear();
|
||||
QString err;
|
||||
devList = CommPort::listCommPorts(err);
|
||||
if (err != "") {
|
||||
QString msg = "Warning:\n\n" + err + "You may need to (re)install "
|
||||
"the FTDI drivers before downloading.";
|
||||
QMessageBox::warning(0, "Error Loading Device Drivers", msg,
|
||||
QMessageBox::Ok, QMessageBox::NoButton);
|
||||
}
|
||||
for (int i = 0; i < devList.size(); ++i)
|
||||
portCombo->addItem(devList[i]->name());
|
||||
if (portCombo->count() > 0)
|
||||
downloadButton->setFocus();
|
||||
setReadyInstruct();
|
||||
}
|
||||
|
||||
bool
|
||||
DownloadRideDialog::statusCallback(const QString &statusText)
|
||||
{
|
||||
label->setText(statusText);
|
||||
QCoreApplication::processEvents();
|
||||
return !cancelled;
|
||||
}
|
||||
|
||||
void
|
||||
DownloadRideDialog::downloadClicked()
|
||||
{
|
||||
downloadButton->setEnabled(false);
|
||||
rescanButton->setEnabled(false);
|
||||
downloadInProgress = true;
|
||||
CommPortPtr dev;
|
||||
for (int i = 0; i < devList.size(); ++i) {
|
||||
if (devList[i]->name() == portCombo->currentText()) {
|
||||
dev = devList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(dev);
|
||||
QString err;
|
||||
QString tmpname, filename;
|
||||
Device &device = Device::device(deviceCombo->currentText());
|
||||
if (!device.download(
|
||||
dev, home, tmpname, filename,
|
||||
boost::bind(&DownloadRideDialog::statusCallback, this, _1), err))
|
||||
{
|
||||
if (cancelled) {
|
||||
QMessageBox::information(this, tr("Download canceled"),
|
||||
tr("Cancel clicked by user."));
|
||||
cancelled = false;
|
||||
}
|
||||
else {
|
||||
QMessageBox::information(this, tr("Download failed"), err);
|
||||
}
|
||||
downloadInProgress = false;
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
QString filepath = home.absolutePath() + "/" + filename;
|
||||
if (QFile::exists(filepath)) {
|
||||
if (QMessageBox::warning(
|
||||
this,
|
||||
tr("Ride Already Downloaded"),
|
||||
tr("This ride appears to have already ")
|
||||
+ tr("been downloaded. Do you want to ")
|
||||
+ tr("overwrite the previous download?"),
|
||||
tr("&Overwrite"), tr("&Cancel"),
|
||||
QString(), 1, 1) == 1) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __WIN32__
|
||||
// Windows ::rename won't overwrite an existing file.
|
||||
if (QFile::exists(filepath)) {
|
||||
QFile old(filepath);
|
||||
if (!old.remove()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to remove existing file ")
|
||||
+ filepath + ": " + old.error());
|
||||
QFile::remove(tmpname);
|
||||
reject();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Use ::rename() instead of QFile::rename() to get forced overwrite.
|
||||
if (rename(QFile::encodeName(tmpname), QFile::encodeName(filepath)) < 0) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to rename ") + tmpname + tr(" to ")
|
||||
+ filepath + ": " + strerror(errno));
|
||||
QFile::remove(tmpname);
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::information(this, tr("Success"), tr("Download complete."));
|
||||
mainWindow->addRide(filename);
|
||||
|
||||
device.cleanup(dev);
|
||||
|
||||
downloadInProgress = false;
|
||||
accept();
|
||||
}
|
||||
|
||||
void
|
||||
DownloadRideDialog::cancelClicked()
|
||||
{
|
||||
if (!downloadInProgress)
|
||||
reject();
|
||||
else
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* $Id: DownloadRideDialog.h,v 1.4 2006/08/11 20:02:13 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
* Copyright (c) 2006-2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
@@ -21,10 +19,8 @@
|
||||
#ifndef _GC_DownloadRideDialog_h
|
||||
#define _GC_DownloadRideDialog_h 1
|
||||
|
||||
#include "CommPort.h"
|
||||
#include <QtGui>
|
||||
extern "C" {
|
||||
#include "pt.h"
|
||||
}
|
||||
|
||||
class MainWindow;
|
||||
|
||||
@@ -34,39 +30,26 @@ class DownloadRideDialog : public QDialog
|
||||
|
||||
public:
|
||||
DownloadRideDialog(MainWindow *mainWindow, const QDir &home);
|
||||
~DownloadRideDialog();
|
||||
|
||||
void time_cb(struct tm *time);
|
||||
void record_cb(unsigned char *buf);
|
||||
void downloadFinished();
|
||||
bool statusCallback(const QString &statusText);
|
||||
|
||||
private slots:
|
||||
void downloadClicked();
|
||||
void cancelClicked();
|
||||
void setReadyInstruct();
|
||||
void scanDevices();
|
||||
void readVersion();
|
||||
void readData();
|
||||
void versionTimeout();
|
||||
void scanCommPorts();
|
||||
|
||||
private:
|
||||
|
||||
MainWindow *mainWindow;
|
||||
QDir home;
|
||||
QListWidget *listWidget;
|
||||
QPushButton *downloadButton, *rescanButton, *cancelButton;
|
||||
QComboBox *portCombo, *deviceCombo;
|
||||
QLabel *label;
|
||||
int endingOffset;
|
||||
int fd;
|
||||
FILE *out;
|
||||
char outname[24];
|
||||
|
||||
char *device;
|
||||
struct pt_read_version_state vstate;
|
||||
struct pt_read_data_state dstate;
|
||||
QSocketNotifier *notifier;
|
||||
QTimer *timer;
|
||||
int blockCount;
|
||||
int hwecho;
|
||||
QVector<CommPortPtr> devList;
|
||||
bool cancelled, downloadInProgress;
|
||||
};
|
||||
|
||||
#endif // _GC_DownloadRideDialog_h
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: LogTimeScaleDraw.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
@@ -23,7 +21,6 @@
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the Qwt License, Version 1.0
|
||||
*
|
||||
*/
|
||||
|
||||
#include "LogTimeScaleDraw.h"
|
||||
@@ -98,8 +95,8 @@ LogTimeScaleDraw::tickLabel(const QFont &font, double value) const
|
||||
else
|
||||
lbl = label(value);
|
||||
|
||||
lbl.setFlags(0);
|
||||
lbl.setLayoutAttributes(QwtText::MinimumLayout);
|
||||
lbl.setRenderFlags(0);
|
||||
lbl.setLayoutAttribute(QwtText::MinimumLayout);
|
||||
|
||||
(void)lbl.textSize(font); // initialize the internal cache
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: LogTimeScaleDraw.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
@@ -23,7 +21,6 @@
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the Qwt License, Version 1.0
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _GC_LogTimeScaleDraw_h
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: LogTimeScaleEngine.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
@@ -23,7 +21,6 @@
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the Qwt License, Version 1.0
|
||||
*
|
||||
*/
|
||||
|
||||
#include "LogTimeScaleEngine.h"
|
||||
@@ -34,10 +31,10 @@
|
||||
/*!
|
||||
Return a transformation, for logarithmic (base 10) scales
|
||||
*/
|
||||
QwtScaleTransformation LogTimeScaleEngine::transformation() const
|
||||
QwtScaleTransformation *LogTimeScaleEngine::transformation() const
|
||||
{
|
||||
return QwtScaleTransformation(QwtScaleTransformation::log10XForm,
|
||||
QwtScaleTransformation::log10InvXForm);
|
||||
return new QwtScaleTransformation(QwtScaleTransformation::Log10);
|
||||
//, log10XForm, QwtScaleTransformation::log10InvXForm);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -56,8 +53,16 @@ void LogTimeScaleEngine::autoScale(int maxNumSteps,
|
||||
if ( x1 > x2 )
|
||||
qSwap(x1, x2);
|
||||
|
||||
QwtDoubleInterval interval(x1 / pow(10.0, loMargin()),
|
||||
x2 * pow(10.0, hiMargin()) );
|
||||
QwtDoubleInterval interval(
|
||||
|
||||
#if (QWT_VERSION >= 0x050200)
|
||||
x1 / pow(10.0, lowerMargin()),
|
||||
x2 * pow(10.0, upperMargin())
|
||||
#else
|
||||
x1 / pow(10.0, loMargin()),
|
||||
x2 * pow(10.0, hiMargin())
|
||||
#endif
|
||||
);
|
||||
|
||||
double logRef = 1.0;
|
||||
if (reference() > LOG_MIN / 2)
|
||||
@@ -73,7 +78,7 @@ void LogTimeScaleEngine::autoScale(int maxNumSteps,
|
||||
if (testAttribute(QwtScaleEngine::IncludeReference))
|
||||
interval = interval.extend(logRef);
|
||||
|
||||
interval = interval.limit(LOG_MIN, LOG_MAX);
|
||||
interval = interval.limited(LOG_MIN, LOG_MAX);
|
||||
|
||||
if (interval.width() == 0.0)
|
||||
interval = buildInterval(interval.minValue());
|
||||
@@ -111,7 +116,7 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
|
||||
int maxMajSteps, int maxMinSteps, double stepSize) const
|
||||
{
|
||||
QwtDoubleInterval interval = QwtDoubleInterval(x1, x2).normalized();
|
||||
interval = interval.limit(LOG_MIN, LOG_MAX);
|
||||
interval = interval.limited(LOG_MIN, LOG_MAX);
|
||||
|
||||
if (interval.width() <= 0 )
|
||||
return QwtScaleDiv();
|
||||
@@ -123,7 +128,13 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
|
||||
QwtLinearScaleEngine linearScaler;
|
||||
linearScaler.setAttributes(attributes());
|
||||
linearScaler.setReference(reference());
|
||||
linearScaler.setMargins(loMargin(), hiMargin());
|
||||
linearScaler.setMargins(
|
||||
#if (QWT_VERSION >= 0x050200)
|
||||
lowerMargin(), upperMargin()
|
||||
#else
|
||||
loMargin(), hiMargin()
|
||||
#endif
|
||||
);
|
||||
|
||||
return linearScaler.divideScale(x1, x2,
|
||||
maxMajSteps, maxMinSteps, stepSize);
|
||||
@@ -143,7 +154,7 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
|
||||
QwtScaleDiv scaleDiv;
|
||||
if ( stepSize != 0.0 )
|
||||
{
|
||||
QwtTickList ticks[QwtScaleDiv::NTickTypes];
|
||||
QwtValueList ticks[QwtScaleDiv::NTickTypes];
|
||||
buildTicks(interval, stepSize, maxMinSteps, ticks);
|
||||
|
||||
scaleDiv = QwtScaleDiv(interval, ticks);
|
||||
@@ -157,7 +168,7 @@ QwtScaleDiv LogTimeScaleEngine::divideScale(double x1, double x2,
|
||||
|
||||
void LogTimeScaleEngine::buildTicks(
|
||||
const QwtDoubleInterval& interval, double stepSize, int maxMinSteps,
|
||||
QwtTickList ticks[QwtScaleDiv::NTickTypes]) const
|
||||
QwtValueList ticks[QwtScaleDiv::NTickTypes]) const
|
||||
{
|
||||
const QwtDoubleInterval boundingInterval =
|
||||
align(interval, stepSize);
|
||||
@@ -181,7 +192,7 @@ struct tick_info_t {
|
||||
};
|
||||
|
||||
tick_info_t tick_info[] = {
|
||||
{ 0.021, "1.26s" },
|
||||
{ 1.0/60.0, "1s" },
|
||||
{ 5.0/60.0, "5s" },
|
||||
{ 15.0/60.0, "15s" },
|
||||
{ 0.5, "30s" },
|
||||
@@ -199,10 +210,12 @@ tick_info_t tick_info[] = {
|
||||
{ -1.0, NULL }
|
||||
};
|
||||
|
||||
QwtTickList LogTimeScaleEngine::buildMajorTicks(
|
||||
QwtValueList LogTimeScaleEngine::buildMajorTicks(
|
||||
const QwtDoubleInterval &interval, double stepSize) const
|
||||
{
|
||||
QwtTickList ticks;
|
||||
(void) interval;
|
||||
(void) stepSize;
|
||||
QwtValueList ticks;
|
||||
tick_info_t *walker = tick_info;
|
||||
while (walker->label) {
|
||||
ticks += walker->x;
|
||||
@@ -211,11 +224,14 @@ QwtTickList LogTimeScaleEngine::buildMajorTicks(
|
||||
return ticks;
|
||||
}
|
||||
|
||||
QwtTickList LogTimeScaleEngine::buildMinorTicks(
|
||||
const QwtTickList &majorTicks,
|
||||
QwtValueList LogTimeScaleEngine::buildMinorTicks(
|
||||
const QwtValueList &majorTicks,
|
||||
int maxMinSteps, double stepSize) const
|
||||
{
|
||||
QwtTickList minorTicks;
|
||||
(void) majorTicks;
|
||||
(void) maxMinSteps;
|
||||
(void) stepSize;
|
||||
QwtValueList minorTicks;
|
||||
return minorTicks;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* $Id: LogTimeScaleEngine.h,v 1.2 2006/07/12 02:13:57 srhea Exp $
|
||||
*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
@@ -23,7 +21,6 @@
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the Qwt License, Version 1.0
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _GC_LogTimeScaleEngine_h
|
||||
@@ -41,7 +38,7 @@ class LogTimeScaleEngine : public QwtScaleEngine
|
||||
int numMajorSteps, int numMinorSteps,
|
||||
double stepSize = 0.0) const;
|
||||
|
||||
virtual QwtScaleTransformation transformation() const;
|
||||
virtual QwtScaleTransformation *transformation() const;
|
||||
|
||||
protected:
|
||||
QwtDoubleInterval log10(const QwtDoubleInterval&) const;
|
||||
@@ -53,13 +50,13 @@ class LogTimeScaleEngine : public QwtScaleEngine
|
||||
|
||||
void buildTicks(
|
||||
const QwtDoubleInterval &, double stepSize, int maxMinSteps,
|
||||
QwtTickList ticks[QwtScaleDiv::NTickTypes]) const;
|
||||
QwtValueList ticks[QwtScaleDiv::NTickTypes]) const;
|
||||
|
||||
QwtTickList buildMinorTicks(
|
||||
const QwtTickList& majorTicks,
|
||||
QwtValueList buildMinorTicks(
|
||||
const QwtValueList& majorTicks,
|
||||
int maxMinMark, double step) const;
|
||||
|
||||
QwtTickList buildMajorTicks(
|
||||
QwtValueList buildMajorTicks(
|
||||
const QwtDoubleInterval &interval, double stepSize) const;
|
||||
};
|
||||
|
||||
2215
src/MainWindow.cpp
Normal file
183
src/MainWindow.h
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_MainWindow_h
|
||||
#define _GC_MainWindow_h 1
|
||||
|
||||
#include <QDir>
|
||||
#include <QtGui>
|
||||
#include <qwt_plot.h>
|
||||
#include <qwt_plot_curve.h>
|
||||
#include "RideItem.h"
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
class AllPlot;
|
||||
class CpintPlot;
|
||||
class PfPvPlot;
|
||||
class PowerHist;
|
||||
class QwtPlotPanner;
|
||||
class QwtPlotPicker;
|
||||
class QwtPlotZoomer;
|
||||
class RideFile;
|
||||
class Zones;
|
||||
class RideCalendar;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(const QDir &home);
|
||||
void addRide(QString name, bool bSelect=true);
|
||||
void removeCurrentRide();
|
||||
const RideFile *currentRide();
|
||||
void getBSFactors(float &timeBS, float &distanceBS);
|
||||
QDir home;
|
||||
|
||||
protected:
|
||||
virtual void resizeEvent(QResizeEvent*);
|
||||
virtual void moveEvent(QMoveEvent*);
|
||||
virtual void closeEvent(QCloseEvent*);
|
||||
|
||||
private slots:
|
||||
void rideSelected();
|
||||
void leftLayoutMoved();
|
||||
void splitterMoved();
|
||||
void newCyclist();
|
||||
void openCyclist();
|
||||
void downloadRide();
|
||||
void manualRide();
|
||||
void exportCSV();
|
||||
void exportXML();
|
||||
void importCSV();
|
||||
void importSRM();
|
||||
void importTCX();
|
||||
void importWKO();
|
||||
void importPolar();
|
||||
void importQuarq();
|
||||
void findBestIntervals();
|
||||
void splitRide();
|
||||
void deleteRide();
|
||||
void setAllPlotWidgets(RideItem *rideItem);
|
||||
void setHistWidgets(RideItem *rideItem);
|
||||
void setSmoothingFromSlider();
|
||||
void setSmoothingFromLineEdit();
|
||||
void cpintSetCPButtonClicked();
|
||||
void setBinWidthFromSlider();
|
||||
void setBinWidthFromLineEdit();
|
||||
void setlnYHistFromCheckBox();
|
||||
void setWithZerosFromCheckBox();
|
||||
void setHistSelection(int id);
|
||||
void setQaCPFromLineEdit();
|
||||
void setQaCADFromLineEdit();
|
||||
void setQaCLFromLineEdit();
|
||||
void setShadeZonesPfPvFromCheckBox();
|
||||
void tabChanged(int index);
|
||||
void pickerMoved(const QPoint &);
|
||||
void aboutDialog();
|
||||
void notesChanged();
|
||||
void saveNotes();
|
||||
void showOptions();
|
||||
void showTools();
|
||||
void importRideToDB();
|
||||
void scanForMissing();
|
||||
void generateWeeklySummary();
|
||||
void dateChanged(const QDate &);
|
||||
|
||||
protected:
|
||||
|
||||
static QString notesFileName(QString rideFileName);
|
||||
|
||||
private:
|
||||
bool parseRideFileName(const QString &name, QString *notesFileName, QDateTime *dt);
|
||||
void setHistBinWidthText();
|
||||
void setHistTextValidator();
|
||||
|
||||
boost::shared_ptr<QSettings> settings;
|
||||
|
||||
RideCalendar *calendar;
|
||||
QSplitter *splitter;
|
||||
QTreeWidget *treeWidget;
|
||||
QTabWidget *tabWidget;
|
||||
QTextEdit *rideSummary;
|
||||
QTextEdit *weeklySummary;
|
||||
AllPlot *allPlot;
|
||||
QwtPlotZoomer *allZoomer;
|
||||
QwtPlotPanner *allPanner;
|
||||
QCheckBox *showHr;
|
||||
QCheckBox *showSpeed;
|
||||
QCheckBox *showCad;
|
||||
QCheckBox *showAlt;
|
||||
QComboBox *showPower;
|
||||
CpintPlot *cpintPlot;
|
||||
QLineEdit *cpintTimeValue;
|
||||
QLineEdit *cpintTodayValue;
|
||||
QLineEdit *cpintAllValue;
|
||||
QPushButton *cpintSetCPButton;
|
||||
QwtPlotPicker *picker;
|
||||
QSlider *smoothSlider;
|
||||
QLineEdit *smoothLineEdit;
|
||||
QSlider *binWidthSlider;
|
||||
QLineEdit *binWidthLineEdit;
|
||||
QCheckBox *lnYHistCheckBox;
|
||||
QCheckBox *withZerosCheckBox;
|
||||
QComboBox *histParameterCombo;
|
||||
QCheckBox *shadeZonesPfPvCheckBox;
|
||||
QTreeWidgetItem *allRides;
|
||||
PowerHist *powerHist;
|
||||
QwtPlot *weeklyPlot;
|
||||
QwtPlotCurve *weeklyDistCurve;
|
||||
QwtPlotCurve *weeklyDurationCurve;
|
||||
QwtPlotCurve *weeklyBaselineCurve;
|
||||
QwtPlotCurve *weeklyBSBaselineCurve;
|
||||
QwtPlot *weeklyBSPlot;
|
||||
QSplitter *leftLayout;
|
||||
|
||||
QwtPlotCurve *weeklyBSCurve;
|
||||
QwtPlotCurve *weeklyRICurve;
|
||||
|
||||
Zones *zones;
|
||||
|
||||
// pedal force/pedal velocity scatter plot widgets
|
||||
PfPvPlot *pfPvPlot;
|
||||
QLineEdit *qaCPValue;
|
||||
QLineEdit *qaCadValue;
|
||||
QLineEdit *qaClValue;
|
||||
|
||||
QTextEdit *rideNotes;
|
||||
QString currentNotesFile;
|
||||
bool currentNotesChanged;
|
||||
|
||||
RideItem *ride; // the currently selected ride
|
||||
|
||||
int histWattsShadedID;
|
||||
int histWattsUnshadedID;
|
||||
int histNmID;
|
||||
int histHrID;
|
||||
int histKphID;
|
||||
int histCadID;
|
||||
int histAltID;
|
||||
|
||||
bool useMetricUnits; // whether metric units are used (or imperial)
|
||||
|
||||
float timebsfactor;
|
||||
float distancebsfactor;
|
||||
};
|
||||
|
||||
#endif // _GC_MainWindow_h
|
||||
|
||||
34
src/Makefile
@@ -1,34 +0,0 @@
|
||||
#
|
||||
# $Id: Makefile,v 1.11 2006/09/06 23:23:03 srhea Exp $
|
||||
#
|
||||
# Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by the Free
|
||||
# Software Foundation; either version 2 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#
|
||||
|
||||
SUBDIRS=lib cmd gui # order is important!
|
||||
|
||||
all: gui-makefile subdirs
|
||||
.PHONY: all gui-makefile subdirs clean
|
||||
|
||||
gui-makefile:
|
||||
cd gui; qmake GoldenCheetah.pro
|
||||
|
||||
clean:
|
||||
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir clean; done
|
||||
|
||||
subdirs:
|
||||
@for dir in $(SUBDIRS); do $(MAKE) -wC $$dir; done
|
||||
|
||||
372
src/ManualRideDialog.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* $Id:$
|
||||
*
|
||||
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "ManualRideDialog.h"
|
||||
#include "MainWindow.h"
|
||||
#include "Settings.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <QtGui>
|
||||
#include <math.h>
|
||||
#include <boost/bind.hpp>
|
||||
|
||||
|
||||
ManualRideDialog::ManualRideDialog(MainWindow *mainWindow,
|
||||
const QDir &home, bool useMetric) :
|
||||
mainWindow(mainWindow), home(home)
|
||||
{
|
||||
useMetricUnits = useMetric;
|
||||
int row;
|
||||
|
||||
|
||||
mainWindow->getBSFactors(timeBS,distanceBS);
|
||||
|
||||
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle(tr("Manually Enter Ride Data"));
|
||||
|
||||
// ride date
|
||||
QLabel *manualDateLabel = new QLabel(tr("Ride date: "), this);
|
||||
dateTimeEdit = new QDateTimeEdit( QDateTime::currentDateTime(), this );
|
||||
// Wed 6/24/09 6:55 AM
|
||||
dateTimeEdit->setDisplayFormat(tr("ddd MMM d, yyyy h:mm AP"));
|
||||
|
||||
// ride length
|
||||
QLabel *manualLengthLabel = new QLabel(tr("Ride length: "), this);
|
||||
QHBoxLayout *manualLengthLayout = new QHBoxLayout;
|
||||
hrslbl = new QLabel(tr("hours"),this);
|
||||
hrslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
hrsentry = new QLineEdit(this);
|
||||
QIntValidator * hoursValidator = new QIntValidator(0,99,this);
|
||||
//hrsentry->setInputMask("09");
|
||||
hrsentry->setValidator(hoursValidator);
|
||||
manualLengthLayout->addWidget(hrslbl);
|
||||
manualLengthLayout->addWidget(hrsentry);
|
||||
|
||||
minslbl = new QLabel(tr("mins"),this);
|
||||
minslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
minsentry = new QLineEdit(this);
|
||||
QIntValidator * secsValidator = new QIntValidator(0,60,this);
|
||||
//minsentry->setInputMask("00");
|
||||
minsentry->setValidator(secsValidator);
|
||||
manualLengthLayout->addWidget(minslbl);
|
||||
manualLengthLayout->addWidget(minsentry);
|
||||
|
||||
secslbl = new QLabel(tr("secs"),this);
|
||||
secslbl->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
secsentry = new QLineEdit(this);
|
||||
//secsentry->setInputMask("00");
|
||||
secsentry->setValidator(secsValidator);
|
||||
manualLengthLayout->addWidget(secslbl);
|
||||
manualLengthLayout->addWidget(secsentry);
|
||||
|
||||
// ride distance
|
||||
QString *DistanceString = new QString(tr("Distance "));
|
||||
if (useMetricUnits)
|
||||
DistanceString->append("(" + tr("km") + "):");
|
||||
else
|
||||
DistanceString->append("(" + tr("miles") + "):");
|
||||
|
||||
QLabel *DistanceLabel = new QLabel(*DistanceString, this);
|
||||
QDoubleValidator * distanceValidator = new QDoubleValidator(0,1000,2,this);
|
||||
distanceentry = new QLineEdit(this);
|
||||
//distanceentry->setInputMask("009.00");
|
||||
distanceentry->setValidator(distanceValidator);
|
||||
|
||||
// AvgHR
|
||||
QLabel *HRLabel = new QLabel(tr("Average HR: "), this);
|
||||
HRentry = new QLineEdit(this);
|
||||
QIntValidator *hrValidator = new QIntValidator(0,200,this);
|
||||
//HRentry->setInputMask("099");
|
||||
HRentry->setValidator(hrValidator);
|
||||
|
||||
// how to estimate BikeScore:
|
||||
QLabel *BSEstLabel;
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
|
||||
QVariant BSmode = settings->value(GC_BIKESCOREMODE);
|
||||
|
||||
|
||||
estBSbyTimeButton = NULL;
|
||||
estBSbyDistButton = NULL;
|
||||
if (timeBS || distanceBS) {
|
||||
BSEstLabel = new QLabel(tr("Estimate BikeScore by: "));
|
||||
if (timeBS) {
|
||||
estBSbyTimeButton = new QRadioButton(tr("Time"));
|
||||
// default to time based unless no timeBS factor
|
||||
if (BSmode.toString() != "distance")
|
||||
estBSbyTimeButton->setDown(true);
|
||||
}
|
||||
if (distanceBS) {
|
||||
estBSbyDistButton = new QRadioButton(tr("Distance"));
|
||||
if (BSmode.toString() == "distance" || ! timeBS)
|
||||
estBSbyDistButton->setDown(true);
|
||||
}
|
||||
}
|
||||
|
||||
// BikeScore
|
||||
QLabel *ManualBSLabel = new QLabel(tr("BikeScore: "), this);
|
||||
BSentry = new QLineEdit(this);
|
||||
BSentry->setInputMask("009");
|
||||
BSentry->clear();
|
||||
|
||||
// buttons
|
||||
enterButton = new QPushButton(tr("&OK"), this);
|
||||
cancelButton = new QPushButton(tr("&Cancel"), this);
|
||||
// don't let Enter write a new (and possibly incomplete) manual file:
|
||||
enterButton->setDefault(false);
|
||||
cancelButton->setDefault(true);
|
||||
|
||||
// Set up the layout:
|
||||
QGridLayout *glayout = new QGridLayout(this);
|
||||
row = 0;
|
||||
glayout->addWidget(manualDateLabel, row, 0);
|
||||
glayout->addWidget(dateTimeEdit, row, 1, 1, -1);
|
||||
row++;
|
||||
|
||||
glayout->addWidget(manualLengthLabel, row, 0);
|
||||
glayout->addLayout(manualLengthLayout,row,1,1,-1);
|
||||
row++;
|
||||
|
||||
glayout->addWidget(DistanceLabel,row,0);
|
||||
glayout->addWidget(distanceentry,row,1,1,-1);
|
||||
row++;
|
||||
|
||||
glayout->addWidget(HRLabel,row,0);
|
||||
glayout->addWidget(HRentry,row,1,1,-1);
|
||||
row++;
|
||||
|
||||
if (timeBS || distanceBS) {
|
||||
glayout->addWidget(BSEstLabel,row,0);
|
||||
if (estBSbyTimeButton)
|
||||
glayout->addWidget(estBSbyTimeButton,row,1,1,-1);
|
||||
if (estBSbyDistButton)
|
||||
glayout->addWidget(estBSbyDistButton,row,2,1,-1);
|
||||
row++;
|
||||
}
|
||||
|
||||
glayout->addWidget(ManualBSLabel,row,0);
|
||||
glayout->addWidget(BSentry,row,1,1,-1);
|
||||
row++;
|
||||
|
||||
glayout->addWidget(enterButton,row,1);
|
||||
glayout->addWidget(cancelButton,row,2);
|
||||
|
||||
this->resize(QSize(400,275));
|
||||
|
||||
connect(enterButton, SIGNAL(clicked()), this, SLOT(enterClicked()));
|
||||
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
|
||||
connect(hrsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
|
||||
//connect(secsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
|
||||
connect(minsentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
|
||||
connect(distanceentry, SIGNAL(editingFinished()), this, SLOT(setBsEst()));
|
||||
if (estBSbyTimeButton)
|
||||
connect(estBSbyTimeButton,SIGNAL(clicked()),this, SLOT(bsEstChanged()));
|
||||
if (estBSbyDistButton)
|
||||
connect(estBSbyDistButton,SIGNAL(clicked()),this, SLOT(bsEstChanged()));
|
||||
}
|
||||
|
||||
void
|
||||
ManualRideDialog::estBSFromDistance()
|
||||
{
|
||||
// calculate distance-based BS estimate
|
||||
double bs = 0;
|
||||
bs = distanceentry->text().toFloat() * distanceBS;
|
||||
QString text = QString("%1").arg((int)bs);
|
||||
// cast to int so QLineEdit doesn't interpret "51.3" as "513"
|
||||
BSentry->clear();
|
||||
BSentry->insert(text);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ManualRideDialog::estBSFromTime()
|
||||
{
|
||||
// calculate time-based BS estimate
|
||||
double bs = 0;
|
||||
bs = ((hrsentry->text().toInt() ) +
|
||||
(minsentry->text().toInt() / 60) +
|
||||
(secsentry->text().toInt()/ 3600)) * timeBS;
|
||||
QString text = QString("%1").arg((int)bs);
|
||||
BSentry->clear();
|
||||
BSentry->insert(text);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ManualRideDialog::bsEstChanged()
|
||||
{
|
||||
if (estBSbyDistButton->isChecked()) {
|
||||
estBSFromDistance();
|
||||
}
|
||||
else {
|
||||
estBSFromTime();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ManualRideDialog::setBsEst()
|
||||
{
|
||||
if (estBSbyDistButton) {
|
||||
if (estBSbyDistButton->isChecked()) {
|
||||
estBSFromDistance();
|
||||
}
|
||||
else {
|
||||
estBSFromTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ManualRideDialog::cancelClicked()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
|
||||
void
|
||||
ManualRideDialog::enterClicked()
|
||||
{
|
||||
// write data to manual entry file
|
||||
|
||||
if (filename == "") {
|
||||
char tmp[32];
|
||||
|
||||
// use user's time for file:
|
||||
QDateTime lt = dateTimeEdit->dateTime().toLocalTime();
|
||||
|
||||
sprintf(tmp, "%04d_%02d_%02d_%02d_%02d_%02d.man",
|
||||
lt.date().year(), lt.date().month(),
|
||||
lt.date().day(), lt.time().hour(),
|
||||
lt.time().minute(),
|
||||
lt.time().second());
|
||||
|
||||
filename = tmp;
|
||||
filepath = home.absolutePath() + "/" + filename;
|
||||
FILE *out = fopen(filepath.toAscii().constData(), "r");
|
||||
if (out) {
|
||||
fclose(out);
|
||||
if (QMessageBox::warning(
|
||||
this,
|
||||
tr("Ride Already Downloaded"),
|
||||
tr("This ride appears to have already ")
|
||||
+ tr("been downloaded. Do you want to ")
|
||||
+ tr("download it again and overwrite ")
|
||||
+ tr("the previous download?"),
|
||||
tr("&Overwrite"), tr("&Cancel"),
|
||||
QString(), 1, 1) == 1) {
|
||||
reject();
|
||||
return ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString tmpname;
|
||||
{
|
||||
// QTemporaryFile doesn't actually close the file on .close(); it
|
||||
// closes the file when in its destructor. On Windows, we can't
|
||||
// rename an open file. So let tmp go out of scope before calling
|
||||
// rename.
|
||||
|
||||
QString tmpl = home.absoluteFilePath(".ptdl.XXXXXX");
|
||||
QTemporaryFile tmp(tmpl);
|
||||
tmp.setAutoRemove(false);
|
||||
if (!tmp.open()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to create temporary file ")
|
||||
+ tmpl + ": " + tmp.error());
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
QTextStream out(&tmp);
|
||||
|
||||
tmpname = tmp.fileName(); // after close(), tmp.fileName() is ""
|
||||
|
||||
/*
|
||||
* File format:
|
||||
* "manual"
|
||||
* "minutes,mph,watts,miles,hr,bikescore" # header (metric or imperial)
|
||||
* minutes,mph,watts,miles,hr,bikeScore # data
|
||||
*/
|
||||
|
||||
out << "manual\n";
|
||||
if (useMetricUnits)
|
||||
out << "minutes,kmh,watts,km,hr,bikescore\n";
|
||||
else
|
||||
out << "minutes,mph,watts,miles,hr,bikescore\n";
|
||||
|
||||
// data
|
||||
double secs = (hrsentry->text().toInt() * 3600) +
|
||||
(minsentry->text().toInt() * 60) +
|
||||
(secsentry->text().toInt());
|
||||
out << secs/60.0;
|
||||
out << ",";
|
||||
out << distanceentry->text().toFloat() / (secs / 3600.0);
|
||||
out << ",";
|
||||
out << 0.0; // watts
|
||||
out << ",";
|
||||
out << distanceentry->text().toFloat();
|
||||
out << ",";
|
||||
out << HRentry->text().toInt();
|
||||
out << ",";
|
||||
out << BSentry->text().toInt();
|
||||
out << "\n";
|
||||
|
||||
|
||||
tmp.close();
|
||||
|
||||
// QTemporaryFile initially has permissions set to 0600.
|
||||
// Make it readable by everyone.
|
||||
tmp.setPermissions(tmp.permissions()
|
||||
| QFile::ReadOwner | QFile::ReadUser
|
||||
| QFile::ReadGroup | QFile::ReadOther);
|
||||
}
|
||||
|
||||
#ifdef __WIN32__
|
||||
// Windows ::rename won't overwrite an existing file.
|
||||
if (QFile::exists(filepath)) {
|
||||
QFile old(filepath);
|
||||
if (!old.remove()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to remove existing file ")
|
||||
+ filepath + ": " + old.error());
|
||||
QFile::remove(tmpname);
|
||||
reject();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Use ::rename() instead of QFile::rename() to get forced overwrite.
|
||||
if (rename(QFile::encodeName(tmpname), QFile::encodeName(filepath)) < 0) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to rename ") + tmpname + tr(" to ")
|
||||
+ filepath + ": " + strerror(errno));
|
||||
QFile::remove(tmpname);
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow->addRide(filename);
|
||||
accept();
|
||||
}
|
||||
|
||||
|
||||
66
src/ManualRideDialog.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_ManualRideDialog_h
|
||||
#define _GC_ManualRideDialog_h 1
|
||||
|
||||
#include <QtGui>
|
||||
#include <qdatetimeedit.h>
|
||||
|
||||
class MainWindow;
|
||||
|
||||
class ManualRideDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ManualRideDialog(MainWindow *mainWindow, const QDir &home,
|
||||
bool useMetric);
|
||||
|
||||
private slots:
|
||||
void enterClicked();
|
||||
void cancelClicked();
|
||||
void bsEstChanged();
|
||||
void setBsEst();
|
||||
|
||||
private:
|
||||
|
||||
void estBSFromDistance();
|
||||
void estBSFromTime();
|
||||
|
||||
bool useMetricUnits;
|
||||
float timeBS, distanceBS;
|
||||
MainWindow *mainWindow;
|
||||
QDir home;
|
||||
QPushButton *enterButton, *cancelButton;
|
||||
QLabel *label;
|
||||
|
||||
QLabel *hrslbl, *minslbl, *secslbl;
|
||||
QLineEdit *hrsentry, *minsentry, *secsentry;
|
||||
QLabel * distancelbl;
|
||||
QLineEdit *distanceentry;
|
||||
QLineEdit *HRentry, *BSentry;
|
||||
QDateTimeEdit *dateTimeEdit;
|
||||
QRadioButton *estBSbyTimeButton, *estBSbyDistButton;
|
||||
|
||||
QVector<unsigned char> records;
|
||||
QString filename, filepath;
|
||||
};
|
||||
|
||||
#endif // _GC_ManualRideDialog_h
|
||||
|
||||
139
src/ManualRideFile.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "ManualRideFile.h"
|
||||
#include <QRegExp>
|
||||
#include <QTextStream>
|
||||
#include <algorithm> // for std::sort
|
||||
#include <assert.h>
|
||||
#include "math.h"
|
||||
|
||||
#define MILES_TO_KM 1.609344
|
||||
#define FEET_TO_METERS 0.3048
|
||||
|
||||
static int manualFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader("man", new ManualFileReader());
|
||||
|
||||
RideFile *ManualFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive);
|
||||
QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive);
|
||||
bool metric;
|
||||
|
||||
int unitsHeader = 2;
|
||||
|
||||
/*
|
||||
* File format:
|
||||
* "manual"
|
||||
* "minutes,mph,watts,miles,hr,bikescore" # header (metric or imperial)
|
||||
* minutes,mph,watts,miles,hr,bikeScore # data
|
||||
*/
|
||||
QRegExp manualCSV("manual", Qt::CaseInsensitive);
|
||||
bool manual = false;
|
||||
|
||||
double rideSec;
|
||||
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
errors << ("Could not open ride file: \""
|
||||
+ file.fileName() + "\"");
|
||||
return NULL;
|
||||
}
|
||||
int lineno = 1;
|
||||
QTextStream is(&file);
|
||||
RideFile *rideFile = new RideFile();
|
||||
while (!is.atEnd()) {
|
||||
// the readLine() method doesn't handle old Macintosh CR line endings
|
||||
// this workaround will load the the entire file if it has CR endings
|
||||
// then split and loop through each line
|
||||
// otherwise, there will be nothing to split and it will read each line as expected.
|
||||
QString linesIn = is.readLine();
|
||||
QStringList lines = linesIn.split('\r');
|
||||
// workaround for empty lines
|
||||
if(lines.isEmpty()) {
|
||||
lineno++;
|
||||
continue;
|
||||
}
|
||||
for (int li = 0; li < lines.size(); ++li) {
|
||||
QString line = lines[li];
|
||||
|
||||
if (lineno == 1) {
|
||||
if (manualCSV.indexIn(line) != -1) {
|
||||
manual = true;
|
||||
rideFile->setDeviceType("Manual CSV");
|
||||
++lineno;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (lineno == unitsHeader) {
|
||||
if (metricUnits.indexIn(line) != -1)
|
||||
metric = true;
|
||||
else if (englishUnits.indexIn(line) != -1)
|
||||
metric = false;
|
||||
else {
|
||||
errors << ("Can't find units in first line: \"" + line + "\"");
|
||||
delete rideFile;
|
||||
file.close();
|
||||
return NULL;
|
||||
}
|
||||
++lineno;
|
||||
continue;
|
||||
}
|
||||
// minutes,kph,watts,km,hr,bikeScore
|
||||
else if (lineno > unitsHeader) {
|
||||
double minutes,kph,watts,km,hr,alt,bs;
|
||||
double cad, nm;
|
||||
int interval;
|
||||
minutes = line.section(',', 0, 0).toDouble();
|
||||
kph = line.section(',', 1, 1).toDouble();
|
||||
watts = line.section(',', 2, 2).toDouble();
|
||||
km = line.section(',', 3, 3).toDouble();
|
||||
hr = line.section(',', 4, 4).toDouble();
|
||||
bs = line.section(',', 5, 5).toDouble();
|
||||
if (!metric) {
|
||||
km *= MILES_TO_KM;
|
||||
kph *= MILES_TO_KM;
|
||||
}
|
||||
cad = nm = 0.0;
|
||||
interval = 0;
|
||||
|
||||
rideFile->appendPoint(minutes * 60.0, cad, hr, km,
|
||||
kph, nm, watts, alt, interval, bs);
|
||||
|
||||
rideSec = minutes * 60.0;
|
||||
}
|
||||
++lineno;
|
||||
}
|
||||
}
|
||||
// fix recording interval at ride length:
|
||||
rideFile->setRecIntSecs(rideSec);
|
||||
|
||||
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
|
||||
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.csv$");
|
||||
if (rideTime.indexIn(file.fileName()) >= 0) {
|
||||
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
|
||||
rideTime.cap(2).toInt(),
|
||||
rideTime.cap(3).toInt()),
|
||||
QTime(rideTime.cap(4).toInt(),
|
||||
rideTime.cap(5).toInt(),
|
||||
rideTime.cap(6).toInt()));
|
||||
rideFile->setStartTime(datetime);
|
||||
}
|
||||
file.close();
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
29
src/ManualRideFile.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Eric Murray (ericm@lne.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _ManualRideFile_h
|
||||
#define _ManualRideFile_h
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
struct ManualFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
};
|
||||
|
||||
#endif // _ManualRideFile_h
|
||||
|
||||
172
src/MetricAggregator.cpp
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#include "MetricAggregator.h"
|
||||
#include "DBAccess.h"
|
||||
#include "RideFile.h"
|
||||
#include "Zones.h"
|
||||
#include "Settings.h"
|
||||
#include "RideItem.h"
|
||||
#include "RideMetric.h"
|
||||
#include "TimeUtils.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <QtXml/QtXml>
|
||||
|
||||
static char rideFileRegExp[] =
|
||||
"^(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)"
|
||||
"_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.(raw|srm|csv|tcx)$";
|
||||
|
||||
MetricAggregator::MetricAggregator()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void MetricAggregator::aggregateRides(QDir home, Zones *zones)
|
||||
{
|
||||
qDebug() << QDateTime::currentDateTime();
|
||||
DBAccess *dbaccess = new DBAccess(home);
|
||||
dbaccess->dropMetricTable();
|
||||
dbaccess->createDatabase();
|
||||
QRegExp rx(rideFileRegExp);
|
||||
QStringList errors;
|
||||
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
|
||||
while (i.hasNext()) {
|
||||
QString name = i.next();
|
||||
QFile file(home.absolutePath() + "/" + name);
|
||||
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
|
||||
importRide(home, zones, ride, name, dbaccess);
|
||||
}
|
||||
dbaccess->closeConnection();
|
||||
delete dbaccess;
|
||||
qDebug() << QDateTime::currentDateTime();
|
||||
|
||||
}
|
||||
|
||||
bool MetricAggregator::importRide(QDir path, Zones *zones, RideFile *ride, QString fileName, DBAccess *dbaccess)
|
||||
{
|
||||
|
||||
SummaryMetrics *summaryMetric = new SummaryMetrics();
|
||||
|
||||
|
||||
QFile file(path.absolutePath() + "/" + fileName);
|
||||
int zone_range = -1;
|
||||
|
||||
QRegExp rx(rideFileRegExp);
|
||||
if (!rx.exactMatch(fileName)) {
|
||||
fprintf(stderr, "bad name: %s\n", fileName.toAscii().constData());
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
summaryMetric->setFileName(fileName);
|
||||
assert(rx.numCaptures() == 7);
|
||||
QDate date(rx.cap(1).toInt(), rx.cap(2).toInt(),rx.cap(3).toInt());
|
||||
QTime time(rx.cap(4).toInt(), rx.cap(5).toInt(),rx.cap(6).toInt());
|
||||
QDateTime dateTime(date, time);
|
||||
|
||||
summaryMetric->setRideDate(dateTime);
|
||||
|
||||
if (zones)
|
||||
zone_range = zones->whichRange(dateTime.date());
|
||||
|
||||
const RideMetricFactory &factory = RideMetricFactory::instance();
|
||||
QSet<QString> todo;
|
||||
|
||||
for (int i = 0; i < factory.metricCount(); ++i)
|
||||
todo.insert(factory.metricName(i));
|
||||
|
||||
|
||||
while (!todo.empty()) {
|
||||
QMutableSetIterator<QString> i(todo);
|
||||
later:
|
||||
while (i.hasNext()) {
|
||||
const QString &name = i.next();
|
||||
const QVector<QString> &deps = factory.dependencies(name);
|
||||
for (int j = 0; j < deps.size(); ++j)
|
||||
if (!metrics.contains(deps[j]))
|
||||
goto later;
|
||||
RideMetric *metric = factory.newMetric(name);
|
||||
metric->compute(ride, zones, zone_range, metrics);
|
||||
metrics.insert(name, metric);
|
||||
i.remove();
|
||||
double value = metric->value(true);
|
||||
if(name == "workout_time")
|
||||
summaryMetric->setWorkoutTime(value);
|
||||
else if(name == "average_cad")
|
||||
summaryMetric->setCadence(value);
|
||||
else if(name == "total_distance")
|
||||
summaryMetric->setDistance(value);
|
||||
else if(name == "skiba_xpower")
|
||||
summaryMetric->setXPower(value);
|
||||
else if(name == "average_speed")
|
||||
summaryMetric->setSpeed(value);
|
||||
else if(name == "total_work")
|
||||
summaryMetric->setTotalWork(value);
|
||||
else if(name == "average_power")
|
||||
summaryMetric->setWatts(value);
|
||||
else if(name == "time_riding")
|
||||
summaryMetric->setRideTime(value);
|
||||
else if(name == "average_hr")
|
||||
summaryMetric->setHeartRate(value);
|
||||
else if(name == "skiba_relative_intensity")
|
||||
summaryMetric->setRelativeIntensity(value);
|
||||
else if(name == "skiba_bike_score")
|
||||
summaryMetric->setBikeScore(value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dbaccess->importRide(summaryMetric);
|
||||
delete summaryMetric;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MetricAggregator::scanForMissing(QDir home, Zones *zones)
|
||||
{
|
||||
QStringList errors;
|
||||
DBAccess *dbaccess = new DBAccess(home);
|
||||
QStringList filenames = dbaccess->getAllFileNames();
|
||||
QRegExp rx(rideFileRegExp);
|
||||
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
|
||||
while (i.hasNext()) {
|
||||
QString name = i.next();
|
||||
if(!filenames.contains(name))
|
||||
{
|
||||
qDebug() << "Found missing file: " << name;
|
||||
QFile file(home.absolutePath() + "/" + name);
|
||||
RideFile *ride = RideFileFactory::instance().openRideFile(file, errors);
|
||||
importRide(home, zones, ride, name, dbaccess);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
dbaccess->closeConnection();
|
||||
delete dbaccess;
|
||||
|
||||
}
|
||||
|
||||
void MetricAggregator::resetMetricTable(QDir home)
|
||||
{
|
||||
DBAccess dbAccess(home);
|
||||
dbAccess.dropMetricTable();
|
||||
}
|
||||
|
||||
49
src/MetricAggregator.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Justin F. Knotzke (jknotzke@shampoo.ca)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
|
||||
#ifndef METRICAGGREGATOR_H_
|
||||
#define METRICAGGREGATOR_H_
|
||||
|
||||
|
||||
#include <QMap>
|
||||
#include "RideFile.h"
|
||||
#include <QDir>
|
||||
#include "Zones.h"
|
||||
#include "RideMetric.h"
|
||||
#include "DBAccess.h"
|
||||
|
||||
|
||||
class MetricAggregator
|
||||
{
|
||||
public:
|
||||
MetricAggregator();
|
||||
void aggregateRides(QDir home, Zones *zones);
|
||||
typedef QHash<QString,RideMetric*> MetricMap;
|
||||
bool importRide(QDir path, Zones *zones, RideFile *ride, QString fileName, DBAccess *dbaccess);
|
||||
MetricMap metrics;
|
||||
void scanForMissing(QDir home, Zones *zones);
|
||||
void resetMetricTable(QDir home);
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif /* METRICAGGREGATOR_H_ */
|
||||
344
src/Pages.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include <QtGui>
|
||||
#include <QIntValidator>
|
||||
#include <assert.h>
|
||||
#include "Pages.h"
|
||||
#include "Settings.h"
|
||||
|
||||
|
||||
ConfigurationPage::~ConfigurationPage()
|
||||
{
|
||||
delete configGroup;
|
||||
delete unitLabel;
|
||||
delete unitCombo;
|
||||
delete allRidesAscending;
|
||||
delete warningLabel;
|
||||
delete unitLayout;
|
||||
delete warningLayout;
|
||||
delete configLayout;
|
||||
delete mainLayout;
|
||||
}
|
||||
|
||||
ConfigurationPage::ConfigurationPage()
|
||||
{
|
||||
configGroup = new QGroupBox(tr("Golden Cheetah Configuration"));
|
||||
|
||||
unitLabel = new QLabel(tr("Unit of Measurement:"));
|
||||
|
||||
unitCombo = new QComboBox();
|
||||
unitCombo->addItem(tr("Metric"));
|
||||
unitCombo->addItem(tr("English"));
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
|
||||
QVariant unit = settings->value(GC_UNIT);
|
||||
|
||||
if(unit.toString() == "Metric")
|
||||
unitCombo->setCurrentIndex(0);
|
||||
else
|
||||
unitCombo->setCurrentIndex(1);
|
||||
|
||||
QLabel *crankLengthLabel = new QLabel(tr("Crank Length:"));
|
||||
|
||||
QVariant crankLength = settings->value(GC_CRANKLENGTH);
|
||||
|
||||
crankLengthCombo = new QComboBox();
|
||||
crankLengthCombo->addItem("160");
|
||||
crankLengthCombo->addItem("162.5");
|
||||
crankLengthCombo->addItem("165");
|
||||
crankLengthCombo->addItem("167.5");
|
||||
crankLengthCombo->addItem("170");
|
||||
crankLengthCombo->addItem("172.5");
|
||||
crankLengthCombo->addItem("175");
|
||||
crankLengthCombo->addItem("177.5");
|
||||
crankLengthCombo->addItem("180");
|
||||
crankLengthCombo->addItem("182.5");
|
||||
crankLengthCombo->addItem("185");
|
||||
if(crankLength.toString() == "160")
|
||||
crankLengthCombo->setCurrentIndex(0);
|
||||
if(crankLength.toString() == "162.5")
|
||||
crankLengthCombo->setCurrentIndex(1);
|
||||
if(crankLength.toString() == "165")
|
||||
crankLengthCombo->setCurrentIndex(2);
|
||||
if(crankLength.toString() == "167.5")
|
||||
crankLengthCombo->setCurrentIndex(3);
|
||||
if(crankLength.toString() == "170")
|
||||
crankLengthCombo->setCurrentIndex(4);
|
||||
if(crankLength.toString() == "172.5")
|
||||
crankLengthCombo->setCurrentIndex(5);
|
||||
if(crankLength.toString() == "175")
|
||||
crankLengthCombo->setCurrentIndex(6);
|
||||
if(crankLength.toString() == "177.5")
|
||||
crankLengthCombo->setCurrentIndex(7);
|
||||
if(crankLength.toString() == "180")
|
||||
crankLengthCombo->setCurrentIndex(8);
|
||||
if(crankLength.toString() == "182.5")
|
||||
crankLengthCombo->setCurrentIndex(9);
|
||||
if(crankLength.toString() == "185")
|
||||
crankLengthCombo->setCurrentIndex(10);
|
||||
|
||||
allRidesAscending = new QCheckBox("Sort ride list ascending.", this);
|
||||
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked); // default is ascending sort
|
||||
if(isAscending.toInt() > 0 ){
|
||||
allRidesAscending->setCheckState(Qt::Checked);
|
||||
} else {
|
||||
allRidesAscending->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
|
||||
warningLabel = new QLabel(tr("Requires Restart To Take Effect"));
|
||||
|
||||
unitLayout = new QHBoxLayout;
|
||||
unitLayout->addWidget(unitLabel);
|
||||
unitLayout->addWidget(unitCombo);
|
||||
|
||||
warningLayout = new QHBoxLayout;
|
||||
warningLayout->addWidget(warningLabel);
|
||||
|
||||
QHBoxLayout *crankLengthLayout = new QHBoxLayout;
|
||||
crankLengthLayout->addWidget(crankLengthLabel);
|
||||
crankLengthLayout->addWidget(crankLengthCombo);
|
||||
|
||||
|
||||
// BikeScore Estimate
|
||||
QVariant BSdays = settings->value(GC_BIKESCOREDAYS);
|
||||
QVariant BSmode = settings->value(GC_BIKESCOREMODE);
|
||||
|
||||
QGridLayout *bsDaysLayout = new QGridLayout;
|
||||
bsModeLayout = new QHBoxLayout;
|
||||
QLabel *BSDaysLabel1 = new QLabel(tr("BikeScore Estimate: use rides within last "));
|
||||
QLabel *BSDaysLabel2 = new QLabel(tr(" days"));
|
||||
BSdaysEdit = new QLineEdit(BSdays.toString(),this);
|
||||
BSdaysEdit->setInputMask("009");
|
||||
|
||||
QLabel *BSModeLabel = new QLabel(tr("BikeScore estimate mode: "));
|
||||
bsModeCombo = new QComboBox();
|
||||
bsModeCombo->addItem("time");
|
||||
bsModeCombo->addItem("distance");
|
||||
if (BSmode.toString() == "time")
|
||||
bsModeCombo->setCurrentIndex(0);
|
||||
else
|
||||
bsModeCombo->setCurrentIndex(1);
|
||||
|
||||
bsDaysLayout->addWidget(BSDaysLabel1,0,0);
|
||||
bsDaysLayout->addWidget(BSdaysEdit,0,1);
|
||||
bsDaysLayout->addWidget(BSDaysLabel2,0,2);
|
||||
|
||||
bsModeLayout->addWidget(BSModeLabel);
|
||||
bsModeLayout->addWidget(bsModeCombo);
|
||||
|
||||
|
||||
|
||||
|
||||
configLayout = new QVBoxLayout;
|
||||
configLayout->addLayout(unitLayout);
|
||||
configLayout->addWidget(allRidesAscending);
|
||||
configLayout->addLayout(crankLengthLayout);
|
||||
configLayout->addLayout(bsDaysLayout);
|
||||
configLayout->addLayout(bsModeLayout);
|
||||
configLayout->addLayout(warningLayout);
|
||||
configGroup->setLayout(configLayout);
|
||||
|
||||
|
||||
mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(configGroup);
|
||||
mainLayout->addStretch(1);
|
||||
setLayout(mainLayout);
|
||||
}
|
||||
|
||||
|
||||
CyclistPage::~CyclistPage()
|
||||
{
|
||||
delete cyclistGroup;
|
||||
delete lblThreshold;
|
||||
delete txtThreshold;
|
||||
delete txtThresholdValidator;
|
||||
delete btnBack;
|
||||
delete btnForward;
|
||||
delete btnDelete;
|
||||
delete checkboxNew;
|
||||
delete txtStartDate;
|
||||
delete txtEndDate;
|
||||
delete lblStartDate;
|
||||
delete lblEndDate;
|
||||
delete calendar;
|
||||
delete lblCurRange;
|
||||
delete powerLayout;
|
||||
delete rangeLayout;
|
||||
delete dateRangeLayout;
|
||||
delete zoneLayout;
|
||||
delete calendarLayout;
|
||||
delete cyclistLayout;
|
||||
delete mainLayout;
|
||||
}
|
||||
|
||||
CyclistPage::CyclistPage(Zones **_zones):
|
||||
zones(_zones)
|
||||
{
|
||||
cyclistGroup = new QGroupBox(tr("Cyclist Options"));
|
||||
lblThreshold = new QLabel(tr("Critical Power:"));
|
||||
txtThreshold = new QLineEdit();
|
||||
|
||||
// the validator will prevent numbers above the upper limit
|
||||
// from being entered, but will not prevent non-negative numbers
|
||||
// below the lower limit (since it is still plausible a valid
|
||||
// entry will result)
|
||||
txtThresholdValidator = new QIntValidator(20,999,this);
|
||||
txtThreshold->setValidator(txtThresholdValidator);
|
||||
|
||||
btnBack = new QPushButton(this);
|
||||
btnBack->setText(tr("Back"));
|
||||
btnForward = new QPushButton(this);
|
||||
btnForward->setText(tr("Forward"));
|
||||
btnDelete = new QPushButton(this);
|
||||
btnDelete->setText(tr("Delete Range"));
|
||||
checkboxNew = new QCheckBox(this);
|
||||
checkboxNew->setText(tr("New Range from Date"));
|
||||
btnForward->setEnabled(false);
|
||||
txtStartDate = new QLabel("BEGIN");
|
||||
txtEndDate = new QLabel("END");
|
||||
lblStartDate = new QLabel("Start: ");
|
||||
lblStartDate->setAlignment(Qt::AlignRight);
|
||||
lblEndDate = new QLabel("End: ");
|
||||
lblEndDate->setAlignment(Qt::AlignRight);
|
||||
|
||||
|
||||
calendar = new QCalendarWidget(this);
|
||||
|
||||
lblCurRange = new QLabel(this);
|
||||
lblCurRange->setFrameStyle(QFrame::Panel | QFrame::Sunken);
|
||||
lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1));
|
||||
|
||||
QDate today = QDate::currentDate();
|
||||
calendar->setSelectedDate(today);
|
||||
|
||||
if ((! *zones) || ((*zones)->getRangeSize() == 0))
|
||||
setCurrentRange();
|
||||
else
|
||||
{
|
||||
setCurrentRange((*zones)->whichRange(today));
|
||||
btnDelete->setEnabled(true);
|
||||
checkboxNew->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
|
||||
int cp = (*zones ? (*zones)->getCP(currentRange) : 0);
|
||||
if (cp > 0)
|
||||
setCP(cp);
|
||||
|
||||
//Layout
|
||||
powerLayout = new QHBoxLayout();
|
||||
powerLayout->addWidget(lblThreshold);
|
||||
powerLayout->addWidget(txtThreshold);
|
||||
|
||||
rangeLayout = new QHBoxLayout();
|
||||
rangeLayout->addWidget(lblCurRange);
|
||||
|
||||
dateRangeLayout = new QHBoxLayout();
|
||||
dateRangeLayout->addWidget(lblStartDate);
|
||||
dateRangeLayout->addWidget(txtStartDate);
|
||||
dateRangeLayout->addWidget(lblEndDate);
|
||||
dateRangeLayout->addWidget(txtEndDate);
|
||||
|
||||
zoneLayout = new QHBoxLayout();
|
||||
zoneLayout->addWidget(btnBack);
|
||||
zoneLayout->addWidget(btnForward);
|
||||
zoneLayout->addWidget(btnDelete);
|
||||
zoneLayout->addWidget(checkboxNew);
|
||||
|
||||
calendarLayout = new QHBoxLayout();
|
||||
calendarLayout->addWidget(calendar);
|
||||
|
||||
cyclistLayout = new QVBoxLayout;
|
||||
cyclistLayout->addLayout(powerLayout);
|
||||
cyclistLayout->addLayout(rangeLayout);
|
||||
cyclistLayout->addLayout(zoneLayout);
|
||||
cyclistLayout->addLayout(dateRangeLayout);
|
||||
cyclistLayout->addLayout(calendarLayout);
|
||||
|
||||
cyclistGroup->setLayout(cyclistLayout);
|
||||
|
||||
mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(cyclistGroup);
|
||||
mainLayout->addStretch(1);
|
||||
setLayout(mainLayout);
|
||||
}
|
||||
|
||||
QString CyclistPage::getText()
|
||||
{
|
||||
return txtThreshold->text();
|
||||
}
|
||||
|
||||
int CyclistPage::getCP()
|
||||
{
|
||||
int cp = txtThreshold->text().toInt();
|
||||
return (
|
||||
(
|
||||
(cp >= txtThresholdValidator->bottom()) &&
|
||||
(cp <= txtThresholdValidator->top())
|
||||
) ?
|
||||
cp :
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
void CyclistPage::setCP(int cp)
|
||||
{
|
||||
txtThreshold->setText(QString("%1").arg(cp));
|
||||
}
|
||||
|
||||
void CyclistPage::setSelectedDate(QDate date)
|
||||
{
|
||||
calendar->setSelectedDate(date);
|
||||
}
|
||||
|
||||
void CyclistPage::setCurrentRange(int range)
|
||||
{
|
||||
int num_ranges =
|
||||
*zones ?
|
||||
(*zones)->getRangeSize() :
|
||||
0;
|
||||
|
||||
if ((num_ranges == 0) || (range < 0)) {
|
||||
btnBack->setEnabled(false);
|
||||
btnDelete->setEnabled(false);
|
||||
btnForward->setEnabled(false);
|
||||
calendar->setEnabled(false);
|
||||
checkboxNew->setCheckState(Qt::Checked);
|
||||
checkboxNew->setEnabled(false);
|
||||
currentRange = -1;
|
||||
lblCurRange->setText("no Current Zone Range");
|
||||
txtEndDate->setText("undefined");
|
||||
txtStartDate->setText("undefined");
|
||||
return;
|
||||
}
|
||||
|
||||
assert ((range >= 0) && (range < num_ranges));
|
||||
currentRange = range;
|
||||
|
||||
// update the labels
|
||||
lblCurRange->setText(QString("Current Zone Range: %1").arg(currentRange + 1));
|
||||
|
||||
// update the visibility of the range select buttons
|
||||
btnForward->setEnabled(currentRange < num_ranges - 1);
|
||||
btnBack->setEnabled(currentRange > 0);
|
||||
|
||||
// if we have ranges to set to, then the calendar must be on
|
||||
calendar->setEnabled(true);
|
||||
|
||||
// update the CP display
|
||||
setCP((*zones)->getCP(currentRange));
|
||||
|
||||
// update date limits
|
||||
txtStartDate->setText((*zones)->getStartDateString(currentRange));
|
||||
txtEndDate->setText((*zones)->getEndDateString(currentRange));
|
||||
}
|
||||
|
||||
|
||||
int CyclistPage::getCurrentRange()
|
||||
{
|
||||
return currentRange;
|
||||
}
|
||||
|
||||
|
||||
bool CyclistPage::isNewMode()
|
||||
{
|
||||
return (checkboxNew->checkState() == Qt::Checked);
|
||||
}
|
||||
94
src/Pages.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#ifndef PAGES_H
|
||||
#define PAGES_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QComboBox>
|
||||
#include <QCalendarWidget>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QList>
|
||||
#include "Zones.h"
|
||||
#include <QLabel>
|
||||
#include <QDateEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QValidator>
|
||||
#include <QGridLayout>
|
||||
|
||||
class QGroupBox;
|
||||
class QHBoxLayout;
|
||||
class QVBoxLayout;
|
||||
|
||||
class ConfigurationPage : public QWidget
|
||||
{
|
||||
public:
|
||||
~ConfigurationPage();
|
||||
ConfigurationPage();
|
||||
QComboBox *unitCombo;
|
||||
QComboBox *crankLengthCombo;
|
||||
QCheckBox *allRidesAscending;
|
||||
QLineEdit *BSdaysEdit;
|
||||
QComboBox *bsModeCombo;
|
||||
|
||||
|
||||
private:
|
||||
QGroupBox *configGroup;
|
||||
QLabel *unitLabel;
|
||||
QLabel *warningLabel;
|
||||
QHBoxLayout *unitLayout;
|
||||
QHBoxLayout *warningLayout;
|
||||
QVBoxLayout *configLayout;
|
||||
QVBoxLayout *mainLayout;
|
||||
QGridLayout *bsDaysLayout;
|
||||
QHBoxLayout *bsModeLayout;
|
||||
};
|
||||
|
||||
class CyclistPage : public QWidget
|
||||
{
|
||||
public:
|
||||
~CyclistPage();
|
||||
CyclistPage(Zones **_zones);
|
||||
int thresholdPower;
|
||||
QString getText();
|
||||
int getCP();
|
||||
void setCP(int cp);
|
||||
void setSelectedDate(QDate date);
|
||||
void setCurrentRange(int range = -1);
|
||||
QPushButton *btnBack;
|
||||
QPushButton *btnForward;
|
||||
QPushButton *btnDelete;
|
||||
QCheckBox *checkboxNew;
|
||||
QCalendarWidget *calendar;
|
||||
QLabel *lblCurRange;
|
||||
QLabel *txtStartDate;
|
||||
QLabel *txtEndDate;
|
||||
QLabel *lblStartDate;
|
||||
QLabel *lblEndDate;
|
||||
|
||||
int getCurrentRange();
|
||||
bool isNewMode();
|
||||
|
||||
inline void setCPFocus() {
|
||||
txtThreshold->setFocus();
|
||||
}
|
||||
|
||||
inline QDate selectedDate() {
|
||||
return calendar->selectedDate();
|
||||
}
|
||||
|
||||
private:
|
||||
QGroupBox *cyclistGroup;
|
||||
Zones **zones;
|
||||
int currentRange;
|
||||
QLabel *lblThreshold;
|
||||
QLineEdit *txtThreshold;
|
||||
QIntValidator *txtThresholdValidator;
|
||||
QHBoxLayout *powerLayout;
|
||||
QHBoxLayout *rangeLayout;
|
||||
QHBoxLayout *dateRangeLayout;
|
||||
QHBoxLayout *zoneLayout;
|
||||
QHBoxLayout *calendarLayout;
|
||||
QVBoxLayout *cyclistLayout;
|
||||
QVBoxLayout *mainLayout;
|
||||
};
|
||||
#endif
|
||||
450
src/PfPvPlot.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net),
|
||||
* J.T Conklin (jtc@acorntoolworks.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "PfPvPlot.h"
|
||||
#include "RideFile.h"
|
||||
#include "RideItem.h"
|
||||
#include "Settings.h"
|
||||
#include "Zones.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
#include <qwt_data.h>
|
||||
#include <qwt_legend.h>
|
||||
#include <qwt_plot_curve.h>
|
||||
#include <qwt_plot_marker.h>
|
||||
#include <qwt_symbol.h>
|
||||
#include <set>
|
||||
|
||||
#define PI M_PI
|
||||
|
||||
|
||||
// Zone labels are drawn if power zone bands are enabled, automatically
|
||||
// at the center of the plot
|
||||
class PfPvPlotZoneLabel: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PfPvPlot *parent;
|
||||
int zone_number;
|
||||
double watts;
|
||||
QwtText text;
|
||||
|
||||
public:
|
||||
PfPvPlotZoneLabel(PfPvPlot *_parent, int _zone_number)
|
||||
{
|
||||
parent = _parent;
|
||||
zone_number = _zone_number;
|
||||
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
setZ(1.0 + zone_number / 100.0);
|
||||
|
||||
// create new zone labels if we're shading
|
||||
if (zones && *zones && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
|
||||
QList <QString> zone_names = (*zones)->getZoneNames(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
assert(zone_names.size() == num_zones);
|
||||
if (zone_number < num_zones) {
|
||||
watts =
|
||||
(
|
||||
(zone_number + 1 < num_zones) ?
|
||||
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
|
||||
(
|
||||
(zone_number > 0) ?
|
||||
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
|
||||
2.0 * zone_lows[zone_number]
|
||||
)
|
||||
);
|
||||
|
||||
text = QwtText(zone_names[zone_number]);
|
||||
text.setFont(QFont("Helvetica",24, QFont::Bold));
|
||||
QColor text_color = zoneColor(zone_number, num_zones);
|
||||
text_color.setAlpha(64);
|
||||
text.setColor(text_color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &yMap,
|
||||
const QRect &rect) const
|
||||
{
|
||||
if (parent->shadeZones() &&
|
||||
(rect.width() > 0) &&
|
||||
(rect.height() > 0)
|
||||
) {
|
||||
// draw the label along a plot diagonal:
|
||||
// 1. x*y = watts * dx/dv * dy/df
|
||||
// 2. x/width = y/height
|
||||
// =>
|
||||
// 1. x^2 = width/height * watts
|
||||
// 2. y^2 = height/width * watts
|
||||
|
||||
double xscale = fabs(xMap.transform(3) - xMap.transform(0)) / 3;
|
||||
double yscale = fabs(yMap.transform(600) - yMap.transform(0)) / 600;
|
||||
if ((xscale > 0) && (yscale > 0)) {
|
||||
double w = watts * xscale * yscale;
|
||||
int x = xMap.transform(sqrt(w * rect.width() / rect.height()) / xscale);
|
||||
int y = yMap.transform(sqrt(w * rect.height() / rect.width()) / yscale);
|
||||
|
||||
// the following code based on source for QwtPlotMarker::draw()
|
||||
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
|
||||
tr.moveCenter(QPoint(x, y));
|
||||
text.draw(painter, tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
QwtArray<double> PfPvPlot::contour_xvalues;
|
||||
|
||||
PfPvPlot::PfPvPlot()
|
||||
: rideItem (NULL),
|
||||
cp_ (0),
|
||||
cad_ (85),
|
||||
cl_ (0.175),
|
||||
shade_zones(true)
|
||||
{
|
||||
setCanvasBackground(Qt::white);
|
||||
|
||||
setAxisTitle(yLeft, "Average Effective Pedal Force (N)");
|
||||
setAxisScale(yLeft, 0, 600);
|
||||
setAxisTitle(xBottom, "Circumferential Pedal Velocity (m/s)");
|
||||
setAxisScale(xBottom, 0, 3);
|
||||
|
||||
mX = new QwtPlotMarker();
|
||||
mX->setLineStyle(QwtPlotMarker::VLine);
|
||||
mX->attach(this);
|
||||
|
||||
mY = new QwtPlotMarker();
|
||||
mY->setLineStyle(QwtPlotMarker::HLine);
|
||||
mY->attach(this);
|
||||
|
||||
cpCurve = new QwtPlotCurve();
|
||||
cpCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
cpCurve->attach(this);
|
||||
|
||||
curve = new QwtPlotCurve();
|
||||
QwtSymbol sym;
|
||||
sym.setStyle(QwtSymbol::Ellipse);
|
||||
sym.setSize(6);
|
||||
sym.setPen(QPen(Qt::red));
|
||||
sym.setBrush(QBrush(Qt::NoBrush));
|
||||
|
||||
curve->setSymbol(sym);
|
||||
curve->setStyle(QwtPlotCurve::Dots);
|
||||
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
curve->attach(this);
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
|
||||
cl_ = settings->value(GC_CRANKLENGTH).toDouble() / 1000.0;
|
||||
|
||||
recalc();
|
||||
}
|
||||
|
||||
void
|
||||
PfPvPlot::refreshZoneItems()
|
||||
{
|
||||
// clear out any zone curves which are presently defined
|
||||
if (zoneCurves.size()) {
|
||||
QListIterator<QwtPlotCurve *> i(zoneCurves);
|
||||
while (i.hasNext()) {
|
||||
QwtPlotCurve *curve = i.next();
|
||||
curve->detach();
|
||||
delete curve;
|
||||
}
|
||||
}
|
||||
zoneCurves.clear();
|
||||
|
||||
|
||||
// delete any existing power zone labels
|
||||
if (zoneLabels.size()) {
|
||||
QListIterator<PfPvPlotZoneLabel *> i(zoneLabels);
|
||||
while (i.hasNext()) {
|
||||
PfPvPlotZoneLabel *label = i.next();
|
||||
label->detach();
|
||||
delete label;
|
||||
}
|
||||
}
|
||||
zoneLabels.clear();
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
if (zones && *zones && (zone_range >= 0)) {
|
||||
setCP((*zones)->getCP(zone_range));
|
||||
|
||||
// populate the zone curves
|
||||
QList <int> zone_power = (*zones)->getZoneLows(zone_range);
|
||||
QList <QString> zone_name = (*zones)->getZoneNames(zone_range);
|
||||
int num_zones = zone_power.size();
|
||||
assert(zone_name.size() == num_zones);
|
||||
if (num_zones > 0) {
|
||||
QPen *pen = new QPen();
|
||||
pen->setStyle(Qt::NoPen);
|
||||
|
||||
|
||||
QwtArray<double> yvalues;
|
||||
|
||||
// generate x values
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
QwtPlotCurve *curve;
|
||||
curve = new QwtPlotCurve(zone_name[z]);
|
||||
curve->setPen(*pen);
|
||||
QColor brush_color = zoneColor(z, num_zones);
|
||||
brush_color.setHsv(
|
||||
brush_color.hue(),
|
||||
brush_color.saturation() / 4,
|
||||
brush_color.value()
|
||||
);
|
||||
curve->setBrush(brush_color); // fill below the line
|
||||
curve->setZ(1 - 1e-6 * zone_power[z]);
|
||||
|
||||
// generate data for curve
|
||||
if (z < num_zones - 1) {
|
||||
QwtArray <double> contour_yvalues;
|
||||
int watts = zone_power[z + 1];
|
||||
int dwatts = (double) watts;
|
||||
for (int i = 0; i < contour_xvalues.size(); i ++)
|
||||
contour_yvalues.append(
|
||||
(1e6 * contour_xvalues[i] < watts) ?
|
||||
1e6 :
|
||||
dwatts / contour_xvalues[i]
|
||||
);
|
||||
curve->setData(contour_xvalues, contour_yvalues);
|
||||
}
|
||||
else {
|
||||
// top zone has a curve at "infinite" power
|
||||
QwtArray <double> contour_x;
|
||||
QwtArray <double> contour_y;
|
||||
contour_x.append(contour_xvalues[0]);
|
||||
contour_x.append(contour_xvalues[contour_xvalues.size() - 1]);
|
||||
contour_y.append(1e6);
|
||||
contour_y.append(1e6);
|
||||
curve->setData(contour_x, contour_y);
|
||||
}
|
||||
curve->setVisible(shade_zones);
|
||||
curve->attach(this);
|
||||
zoneCurves.append(curve);
|
||||
}
|
||||
|
||||
delete pen;
|
||||
|
||||
|
||||
// generate labels for existing zones
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
PfPvPlotZoneLabel *label = new PfPvPlotZoneLabel(this, z);
|
||||
label->setVisible(shade_zones);
|
||||
label->attach(this);
|
||||
zoneLabels.append(label);
|
||||
}
|
||||
// get the zones visible, even if data may take awhile
|
||||
replot();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PfPvPlot::setData(RideItem *_rideItem)
|
||||
{
|
||||
rideItem = _rideItem;
|
||||
|
||||
RideFile *ride = rideItem->ride;
|
||||
|
||||
if (ride) {
|
||||
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
|
||||
|
||||
// quickly erase old data
|
||||
curve->setVisible(false);
|
||||
|
||||
// handle zone stuff
|
||||
refreshZoneItems();
|
||||
|
||||
// due to the discrete power and cadence values returned by the
|
||||
// power meter, there will very likely be many duplicate values.
|
||||
// Rather than pass them all to the curve, use a set to strip
|
||||
// out duplicates.
|
||||
std::set<std::pair<double, double> > dataSet;
|
||||
|
||||
long tot_cad = 0;
|
||||
long tot_cad_points = 0;
|
||||
|
||||
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
while (i.hasNext()) {
|
||||
const RideFilePoint *p1 = i.next();
|
||||
|
||||
if (p1->watts != 0 && p1->cad != 0) {
|
||||
double aepf = (p1->watts * 60.0) / (p1->cad * cl_ * 2.0 * PI);
|
||||
double cpv = (p1->cad * cl_ * 2.0 * PI) / 60.0;
|
||||
|
||||
dataSet.insert(std::make_pair<double, double>(aepf, cpv));
|
||||
|
||||
tot_cad += p1->cad;
|
||||
tot_cad_points++;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (tot_cad_points == 0) {
|
||||
setTitle("no cadence");
|
||||
refreshZoneItems();
|
||||
curve->setVisible(false);
|
||||
}
|
||||
|
||||
else {
|
||||
// Now that we have the set of points, transform them into the
|
||||
// QwtArrays needed to set the curve's data.
|
||||
QwtArray<double> aepfArray;
|
||||
QwtArray<double> cpvArray;
|
||||
std::set<std::pair<double, double> >::const_iterator j(dataSet.begin());
|
||||
while (j != dataSet.end()) {
|
||||
const std::pair<double, double>& dataPoint = *j;
|
||||
|
||||
aepfArray.push_back(dataPoint.first);
|
||||
cpvArray.push_back(dataPoint.second);
|
||||
|
||||
++j;
|
||||
}
|
||||
|
||||
setCAD(tot_cad / tot_cad_points);
|
||||
|
||||
curve->setData(cpvArray, aepfArray);
|
||||
|
||||
// now show the data (zone shading would already be visible)
|
||||
curve->setVisible(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
setTitle("no data");
|
||||
refreshZoneItems();
|
||||
curve->setVisible(false);
|
||||
}
|
||||
|
||||
replot();
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
setCL(settings->value(GC_CRANKLENGTH).toDouble() / 1000.0);
|
||||
}
|
||||
|
||||
void
|
||||
PfPvPlot::recalc()
|
||||
{
|
||||
// initialize x values used for contours
|
||||
if (contour_xvalues.isEmpty()) {
|
||||
for (double x = 0; x <= 3.0; x += x / 20 + 0.02)
|
||||
contour_xvalues.append(x);
|
||||
contour_xvalues.append(3.0);
|
||||
}
|
||||
|
||||
double cpv = (cad_ * cl_ * 2.0 * PI) / 60.0;
|
||||
mX->setXValue(cpv);
|
||||
|
||||
double aepf = (cp_ * 60.0) / (cad_ * cl_ * 2.0 * PI);
|
||||
mY->setYValue(aepf);
|
||||
|
||||
QwtArray<double> yvalues(contour_xvalues.size());
|
||||
if (cp_) {
|
||||
for (int i = 0; i < contour_xvalues.size(); i ++)
|
||||
yvalues[i] =
|
||||
(cpv < cp_ / 1e6) ?
|
||||
1e6 :
|
||||
cp_ / contour_xvalues[i];
|
||||
|
||||
// generate curve at a given power
|
||||
cpCurve->setData(contour_xvalues, yvalues);
|
||||
}
|
||||
else
|
||||
// an empty curve if no power (or zero power) is specified
|
||||
cpCurve->setData(QwtArray <double>::QwtArray(), QwtArray <double>::QwtArray());
|
||||
|
||||
replot();
|
||||
}
|
||||
|
||||
int
|
||||
PfPvPlot::getCP()
|
||||
{
|
||||
return cp_;
|
||||
}
|
||||
|
||||
void
|
||||
PfPvPlot::setCP(int cp)
|
||||
{
|
||||
cp_ = cp;
|
||||
recalc();
|
||||
emit changedCP( QString("%1").arg(cp) );
|
||||
}
|
||||
|
||||
int
|
||||
PfPvPlot::getCAD()
|
||||
{
|
||||
return cad_;
|
||||
}
|
||||
|
||||
void
|
||||
PfPvPlot::setCAD(int cadence)
|
||||
{
|
||||
cad_ = cadence;
|
||||
recalc();
|
||||
emit changedCAD( QString("%1").arg(cadence) );
|
||||
}
|
||||
|
||||
double
|
||||
PfPvPlot::getCL()
|
||||
{
|
||||
return cl_;
|
||||
}
|
||||
|
||||
void
|
||||
PfPvPlot::setCL(double cranklen)
|
||||
{
|
||||
cl_ = cranklen;
|
||||
recalc();
|
||||
emit changedCL( QString("%1").arg(cranklen) );
|
||||
}
|
||||
// process checkbox for zone shading
|
||||
void
|
||||
PfPvPlot::setShadeZones(bool value)
|
||||
{
|
||||
shade_zones = value;
|
||||
|
||||
// if there are defined zones and labels, set their visibility
|
||||
for (int i = 0; i < zoneCurves.size(); i ++)
|
||||
zoneCurves[i]->setVisible(shade_zones);
|
||||
for (int i = 0; i < zoneLabels.size(); i ++)
|
||||
zoneLabels[i]->setVisible(shade_zones);
|
||||
|
||||
replot();
|
||||
}
|
||||
79
src/PfPvPlot.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net),
|
||||
* J.T Conklin (jtc@acorntoolworks.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_QaPlot_h
|
||||
#define _GC_QaPlot_h 1
|
||||
|
||||
#include <qwt_plot.h>
|
||||
|
||||
// forward references
|
||||
class RideFile;
|
||||
class RideItem;
|
||||
class QwtPlotCurve;
|
||||
class QwtPlotMarker;
|
||||
class PfPvPlotZoneLabel;
|
||||
|
||||
class PfPvPlot : public QwtPlot
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
PfPvPlot();
|
||||
void refreshZoneItems();
|
||||
void setData(RideItem *_rideItem);
|
||||
|
||||
int getCP();
|
||||
void setCP(int cp);
|
||||
int getCAD();
|
||||
void setCAD(int cadence);
|
||||
double getCL();
|
||||
void setCL(double cranklen);
|
||||
void recalc();
|
||||
|
||||
RideItem *rideItem;
|
||||
|
||||
bool shadeZones() const { return shade_zones; }
|
||||
void setShadeZones(bool value);
|
||||
|
||||
public slots:
|
||||
signals:
|
||||
|
||||
void changedCP( const QString& );
|
||||
void changedCAD( const QString& );
|
||||
void changedCL( const QString& );
|
||||
|
||||
protected:
|
||||
QwtPlotCurve *curve;
|
||||
QwtPlotCurve *cpCurve;
|
||||
QList <QwtPlotCurve *> zoneCurves;
|
||||
QList <PfPvPlotZoneLabel *> zoneLabels;
|
||||
QwtPlotMarker *mX;
|
||||
QwtPlotMarker *mY;
|
||||
|
||||
static QwtArray<double> contour_xvalues; // values used in CP and contour plots: djconnel
|
||||
|
||||
int cp_;
|
||||
int cad_;
|
||||
double cl_;
|
||||
bool shade_zones; // whether to shade zones, added 27Apr2009 djconnel
|
||||
};
|
||||
|
||||
#endif // _GC_QaPlot_h
|
||||
|
||||
227
src/PolarRideFile.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "PolarRideFile.h"
|
||||
#include <QRegExp>
|
||||
#include <QTextStream>
|
||||
#include <algorithm> // for std::sort
|
||||
#include <assert.h>
|
||||
#include "math.h"
|
||||
|
||||
|
||||
static int polarFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader("hrm", new PolarFileReader());
|
||||
|
||||
RideFile *PolarFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
QRegExp metricUnits("(km|kph|km/h)", Qt::CaseInsensitive);
|
||||
QRegExp englishUnits("(miles|mph|mp/h)", Qt::CaseInsensitive);
|
||||
// bool metric;
|
||||
|
||||
QDate date;
|
||||
QString note("");
|
||||
|
||||
double version=0;
|
||||
|
||||
double seconds=0;
|
||||
double distance=0;
|
||||
int interval = 0;
|
||||
|
||||
bool speed = false;
|
||||
bool cadence = false;
|
||||
bool altitude = false;
|
||||
bool power = false;
|
||||
bool balance = false;
|
||||
bool pedaling_index = false;
|
||||
|
||||
|
||||
int recInterval = 1;
|
||||
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
errors << ("Could not open ride file: \""
|
||||
+ file.fileName() + "\"");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int lineno = 1;
|
||||
|
||||
|
||||
double next_interval=0;
|
||||
QList<double> intervals;
|
||||
|
||||
QTextStream is(&file);
|
||||
RideFile *rideFile = new RideFile();
|
||||
QString section = NULL;
|
||||
|
||||
while (!is.atEnd()) {
|
||||
// the readLine() method doesn't handle old Macintosh CR line endings
|
||||
// this workaround will load the the entire file if it has CR endings
|
||||
// then split and loop through each line
|
||||
// otherwise, there will be nothing to split and it will read each line as expected.
|
||||
QString linesIn = is.readLine();
|
||||
QStringList lines = linesIn.split('\r');
|
||||
// workaround for empty lines
|
||||
if(lines.size() == 0) {
|
||||
lineno++;
|
||||
continue;
|
||||
}
|
||||
for (int li = 0; li < lines.size(); ++li) {
|
||||
QString line = lines[li];
|
||||
|
||||
if (line == "") {
|
||||
|
||||
}
|
||||
else if (line.startsWith("[")) {
|
||||
//fprintf(stderr, "section : %s\n", line.toAscii().constData());
|
||||
section=line;
|
||||
if (section == "[HRData]")
|
||||
next_interval = intervals.at(0);
|
||||
}
|
||||
else if (section == "[Params]"){
|
||||
if (line.contains("Version=")) {
|
||||
QString versionString = QString(line);
|
||||
versionString.remove(0,8).insert(1, ".");
|
||||
version = versionString.toFloat();
|
||||
rideFile->setDeviceType("Polar HRM (v"+versionString+")");
|
||||
|
||||
} else if (line.contains("SMode=")) {
|
||||
line.remove(0,6);
|
||||
QString smode = QString(line);
|
||||
if (smode.at(0)=='1')
|
||||
speed = true;
|
||||
if (smode.length()>0 && smode.at(1)=='1')
|
||||
cadence = true;
|
||||
if (smode.length()>1 && smode.at(2)=='1')
|
||||
altitude = true;
|
||||
if (smode.length()>2 && smode.at(3)=='1')
|
||||
power = true;
|
||||
if (smode.length()>3 && smode.at(4)=='1')
|
||||
balance = true;
|
||||
if (smode.length()>4 && smode.at(5)=='1')
|
||||
pedaling_index = true;
|
||||
|
||||
/*
|
||||
It appears that the Polar CS600 exports its data alays in metric when downloaded from the
|
||||
polar software even when English units are displayed on the unit.. It also never sets
|
||||
this bit low in the .hrm file. This will have to get changed if other software downloads
|
||||
this differently
|
||||
*/
|
||||
|
||||
// if (smode.length()>6 && smode.at(7)=='1')
|
||||
// metric = true;
|
||||
|
||||
} else if (line.contains("Interval=")) {
|
||||
recInterval = line.remove(0,9).toInt();
|
||||
rideFile->setRecIntSecs(recInterval);
|
||||
} else if (line.contains("Date=")) {
|
||||
line.remove(0,5);
|
||||
date= QDate(line.left(4).toInt(),
|
||||
line.mid(4,2).toInt(),
|
||||
line.mid(6,2).toInt());
|
||||
} else if (line.contains("StartTime=")) {
|
||||
line.remove(0,10);
|
||||
QDateTime datetime(date,
|
||||
QTime(line.left(2).toInt(),
|
||||
line.mid(3,2).toInt(),
|
||||
line.mid(6,2).toInt()));
|
||||
rideFile->setStartTime(datetime);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else if (section == "[Note]"){
|
||||
note.append(line);
|
||||
}
|
||||
else if (section == "[IntTimes]"){
|
||||
double int_seconds = line.left(2).toInt()*60*60+line.mid(3,2).toInt()*60+line.mid(6,3).toFloat();
|
||||
intervals.append(int_seconds);
|
||||
|
||||
if (lines.size()==1) {
|
||||
is.readLine();
|
||||
is.readLine();
|
||||
if (version>1.05) {
|
||||
is.readLine();
|
||||
is.readLine();
|
||||
}
|
||||
} else {
|
||||
li+=2;
|
||||
if (version>1.05)
|
||||
li+=2;
|
||||
}
|
||||
}
|
||||
else if (section == "[HRData]"){
|
||||
double nm=0,kph=0,watts=0,km=0,cad=0,hr=0,alt=0;
|
||||
|
||||
seconds += recInterval;
|
||||
|
||||
int i=0;
|
||||
hr = line.section('\t', i, i).toDouble();
|
||||
i++;
|
||||
|
||||
if (speed) {
|
||||
kph = line.section('\t', i, i).toDouble()/10;
|
||||
i++;
|
||||
}
|
||||
if (cadence) {
|
||||
cad = line.section('\t', i, i).toDouble();
|
||||
i++;
|
||||
}
|
||||
if (altitude) {
|
||||
alt = line.section('\t', i, i).toDouble();
|
||||
i++;
|
||||
}
|
||||
if (power) {
|
||||
watts = line.section('\t', i, i).toDouble();
|
||||
}
|
||||
|
||||
distance = distance + kph/60/60*recInterval;
|
||||
km = distance;
|
||||
|
||||
if (next_interval < seconds) {
|
||||
interval = intervals.indexOf(next_interval);
|
||||
if (intervals.count()>interval+1){
|
||||
interval++;
|
||||
next_interval = intervals.at(interval);
|
||||
}
|
||||
}
|
||||
rideFile->appendPoint(seconds, cad, hr, km, kph, nm, watts, alt, interval);
|
||||
//fprintf(stderr, " %f, %f, %f, %f, %f, %f, %f, %d\n", seconds, cad, hr, km, kph, nm, watts, alt, interval);
|
||||
}
|
||||
|
||||
++lineno;
|
||||
}
|
||||
}
|
||||
|
||||
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
|
||||
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.hrm$");
|
||||
if (rideTime.indexIn(file.fileName()) >= 0) {
|
||||
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
|
||||
rideTime.cap(2).toInt(),
|
||||
rideTime.cap(3).toInt()),
|
||||
QTime(rideTime.cap(4).toInt(),
|
||||
rideTime.cap(5).toInt(),
|
||||
rideTime.cap(6).toInt()));
|
||||
|
||||
|
||||
rideFile->setStartTime(datetime);
|
||||
}
|
||||
file.close();
|
||||
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
29
src/PolarRideFile.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _PolarRideFile_h
|
||||
#define _PolarRideFile_h
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
struct PolarFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
};
|
||||
|
||||
#endif // _PolarRideFile_h
|
||||
|
||||
643
src/PowerHist.cpp
Normal file
@@ -0,0 +1,643 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "PowerHist.h"
|
||||
#include "RideItem.h"
|
||||
#include "RideFile.h"
|
||||
#include "Settings.h"
|
||||
#include "Zones.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <qpainter.h>
|
||||
#include <qwt_plot_curve.h>
|
||||
#include <qwt_plot_grid.h>
|
||||
#include <qwt_plot_zoomer.h>
|
||||
#include <qwt_scale_engine.h>
|
||||
#include <qwt_text.h>
|
||||
#include <qwt_legend.h>
|
||||
#include <qwt_data.h>
|
||||
|
||||
class penTooltip: public QwtPlotZoomer
|
||||
{
|
||||
public:
|
||||
penTooltip(QwtPlotCanvas *canvas):
|
||||
QwtPlotZoomer(canvas)
|
||||
{
|
||||
// With some versions of Qt/Qwt, setting this to AlwaysOn
|
||||
// causes an infinite recursion.
|
||||
//setTrackerMode(AlwaysOn);
|
||||
setTrackerMode(AlwaysOff);
|
||||
}
|
||||
|
||||
virtual QwtText trackerText(const QwtDoublePoint &pos) const
|
||||
{
|
||||
QColor bg(Qt::white);
|
||||
#if QT_VERSION >= 0x040300
|
||||
bg.setAlpha(200);
|
||||
#endif
|
||||
|
||||
QwtText text = QString("%1").arg((int)pos.x());
|
||||
text.setBackgroundBrush( QBrush( bg ));
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// define a background class to handle shading of power zones
|
||||
// draws power zone bands IF zones are defined and the option
|
||||
// to draw bonds has been selected
|
||||
class PowerHistBackground: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PowerHist *parent;
|
||||
|
||||
public:
|
||||
PowerHistBackground(PowerHist *_parent)
|
||||
{
|
||||
setZ(0.0);
|
||||
parent = _parent;
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
virtual void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &,
|
||||
const QRect &rect) const
|
||||
{
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
if (num_zones > 0) {
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
QRect r = rect;
|
||||
|
||||
QColor shading_color =
|
||||
zoneColor(z, num_zones);
|
||||
shading_color.setHsv(
|
||||
shading_color.hue(),
|
||||
shading_color.saturation() / 4,
|
||||
shading_color.value()
|
||||
);
|
||||
r.setLeft(xMap.transform(zone_lows[z]));
|
||||
if (z + 1 < num_zones)
|
||||
r.setRight(xMap.transform(zone_lows[z + 1]));
|
||||
if (r.right() >= r.left())
|
||||
painter->fillRect(r, shading_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Zone labels are drawn if power zone bands are enabled, automatically
|
||||
// at the center of the plot
|
||||
class PowerHistZoneLabel: public QwtPlotItem
|
||||
{
|
||||
private:
|
||||
PowerHist *parent;
|
||||
int zone_number;
|
||||
double watts;
|
||||
QwtText text;
|
||||
|
||||
public:
|
||||
PowerHistZoneLabel(PowerHist *_parent, int _zone_number)
|
||||
{
|
||||
parent = _parent;
|
||||
zone_number = _zone_number;
|
||||
|
||||
RideItem *rideItem = parent->rideItem;
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
setZ(1.0 + zone_number / 100.0);
|
||||
|
||||
// create new zone labels if we're shading
|
||||
if (parent->shadeZones() && zones && *zones && (zone_range >= 0)) {
|
||||
QList <int> zone_lows = (*zones)->getZoneLows(zone_range);
|
||||
QList <QString> zone_names = (*zones)->getZoneNames(zone_range);
|
||||
int num_zones = zone_lows.size();
|
||||
assert(zone_names.size() == num_zones);
|
||||
if (zone_number < num_zones) {
|
||||
watts =
|
||||
(
|
||||
(zone_number + 1 < num_zones) ?
|
||||
0.5 * (zone_lows[zone_number] + zone_lows[zone_number + 1]) :
|
||||
(
|
||||
(zone_number > 0) ?
|
||||
(1.5 * zone_lows[zone_number] - 0.5 * zone_lows[zone_number - 1]) :
|
||||
2.0 * zone_lows[zone_number]
|
||||
)
|
||||
);
|
||||
|
||||
text = QwtText(zone_names[zone_number]);
|
||||
text.setFont(QFont("Helvetica",24, QFont::Bold));
|
||||
QColor text_color = zoneColor(zone_number, num_zones);
|
||||
text_color.setAlpha(64);
|
||||
text.setColor(text_color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
virtual int rtti() const
|
||||
{
|
||||
return QwtPlotItem::Rtti_PlotUserItem;
|
||||
}
|
||||
|
||||
void draw(QPainter *painter,
|
||||
const QwtScaleMap &xMap, const QwtScaleMap &,
|
||||
const QRect &rect) const
|
||||
{
|
||||
if (parent->shadeZones()) {
|
||||
int x = xMap.transform(watts);
|
||||
int y = (rect.bottom() + rect.top()) / 2;
|
||||
|
||||
// the following code based on source for QwtPlotMarker::draw()
|
||||
QRect tr(QPoint(0, 0), text.textSize(painter->font()));
|
||||
tr.moveCenter(QPoint(y, -x));
|
||||
painter->rotate(90); // rotate text to avoid overlap: this needs to be fixed
|
||||
text.draw(painter, tr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PowerHist::PowerHist():
|
||||
selected(wattsShaded),
|
||||
rideItem(NULL),
|
||||
binw(20),
|
||||
withz(true),
|
||||
settings(NULL),
|
||||
unit(0),
|
||||
lny(false)
|
||||
{
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
|
||||
unit = settings->value(GC_UNIT);
|
||||
|
||||
useMetricUnits = (unit.toString() == "Metric");
|
||||
|
||||
// create a background object for shading
|
||||
bg = new PowerHistBackground(this);
|
||||
bg->attach(this);
|
||||
|
||||
setCanvasBackground(Qt::white);
|
||||
|
||||
setParameterAxisTitle();
|
||||
setAxisTitle(yLeft, "Cumulative Time (minutes)");
|
||||
|
||||
curve = new QwtPlotCurve("");
|
||||
curve->setStyle(QwtPlotCurve::Steps);
|
||||
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
||||
QPen *pen = new QPen(Qt::black);
|
||||
pen->setWidth(2.0);
|
||||
curve->setPen(*pen);
|
||||
QColor brush_color = Qt::black;
|
||||
brush_color.setAlpha(64);
|
||||
curve->setBrush(brush_color); // fill below the line
|
||||
delete pen;
|
||||
curve->attach(this);
|
||||
|
||||
grid = new QwtPlotGrid();
|
||||
grid->enableX(false);
|
||||
QPen gridPen;
|
||||
gridPen.setStyle(Qt::DotLine);
|
||||
grid->setPen(gridPen);
|
||||
grid->attach(this);
|
||||
|
||||
zoneLabels = QList <PowerHistZoneLabel *>::QList();
|
||||
|
||||
new penTooltip(this->canvas());
|
||||
}
|
||||
|
||||
PowerHist::~PowerHist() {
|
||||
delete bg;
|
||||
delete curve;
|
||||
delete grid;
|
||||
}
|
||||
|
||||
// static const variables from PoweHist.h:
|
||||
// discritized unit for smoothing
|
||||
const double PowerHist::wattsDelta;
|
||||
const double PowerHist::nmDelta;
|
||||
const double PowerHist::hrDelta;
|
||||
const double PowerHist::kphDelta;
|
||||
const double PowerHist::cadDelta;
|
||||
|
||||
// digits for text entry validator
|
||||
const int PowerHist::wattsDigits;
|
||||
const int PowerHist::nmDigits;
|
||||
const int PowerHist::hrDigits;
|
||||
const int PowerHist::kphDigits;
|
||||
const int PowerHist::cadDigits;
|
||||
|
||||
void
|
||||
PowerHist::refreshZoneLabels()
|
||||
{
|
||||
// delete any existing power zone labels
|
||||
if (zoneLabels.size()) {
|
||||
QListIterator<PowerHistZoneLabel *> i(zoneLabels);
|
||||
while (i.hasNext()) {
|
||||
PowerHistZoneLabel *label = i.next();
|
||||
label->detach();
|
||||
delete label;
|
||||
}
|
||||
}
|
||||
zoneLabels.clear();
|
||||
|
||||
if (! rideItem)
|
||||
return;
|
||||
|
||||
if ((selected == wattsShaded) || (selected == wattsUnshaded)) {
|
||||
Zones **zones = rideItem->zones;
|
||||
int zone_range = rideItem->zoneRange();
|
||||
|
||||
// generate labels for existing zones
|
||||
if (zones && *zones && (zone_range >= 0)) {
|
||||
int num_zones = (*zones)->numZones(zone_range);
|
||||
for (int z = 0; z < num_zones; z ++) {
|
||||
PowerHistZoneLabel *label = new PowerHistZoneLabel(this, z);
|
||||
label->attach(this);
|
||||
zoneLabels.append(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::recalc()
|
||||
{
|
||||
QVector<unsigned int> *array;
|
||||
int arrayLength = 0;
|
||||
double delta;
|
||||
|
||||
// make sure the interval length is set
|
||||
if (dt <= 0)
|
||||
return;
|
||||
|
||||
if ((selected == wattsShaded) ||
|
||||
(selected == wattsUnshaded)
|
||||
) {
|
||||
array = &wattsArray;
|
||||
delta = wattsDelta;
|
||||
arrayLength = wattsArray.size();
|
||||
}
|
||||
else if (selected == nm) {
|
||||
array = &nmArray;
|
||||
delta = nmDelta;
|
||||
arrayLength = nmArray.size();
|
||||
}
|
||||
else if (selected == hr) {
|
||||
array = &hrArray;
|
||||
delta = hrDelta;
|
||||
arrayLength = hrArray.size();
|
||||
}
|
||||
else if (selected == kph) {
|
||||
array = &kphArray;
|
||||
delta = kphDelta;
|
||||
arrayLength = kphArray.size();
|
||||
}
|
||||
else if (selected == cad) {
|
||||
array = &cadArray;
|
||||
delta = cadDelta;
|
||||
arrayLength = cadArray.size();
|
||||
}
|
||||
|
||||
if (!array)
|
||||
return;
|
||||
|
||||
int count = int(ceil((arrayLength - 1) / binw));
|
||||
|
||||
// allocate space for data, plus beginning and ending point
|
||||
QVector<double> parameterValue(count+2);
|
||||
QVector<double> totalTime(count+2);
|
||||
int i;
|
||||
for (i = 1; i <= count; ++i) {
|
||||
int high = i * binw;
|
||||
int low = high - binw;
|
||||
if (low==0 && !withz)
|
||||
low++;
|
||||
parameterValue[i] = high * delta;
|
||||
totalTime[i] = 1e-9; // nonzero to accomodate log plot
|
||||
while (low < high)
|
||||
totalTime[i] += dt * (*array)[low++];
|
||||
}
|
||||
totalTime[i] = 1e-9; // nonzero to accomodate log plot
|
||||
parameterValue[i] = i * delta * binw;
|
||||
totalTime[0] = 1e-9;
|
||||
parameterValue[0] = 0;
|
||||
curve->setData(parameterValue.data(), totalTime.data(), count + 2);
|
||||
setAxisScale(xBottom, 0.0, parameterValue[count + 1]);
|
||||
|
||||
refreshZoneLabels();
|
||||
|
||||
setYMax();
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setYMax()
|
||||
{
|
||||
static const double tmin = 1.0/60;
|
||||
setAxisScale(yLeft, (lny ? tmin : 0.0), curve->maxYValue() * 1.1);
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setData(RideItem *_rideItem)
|
||||
{
|
||||
rideItem = _rideItem;
|
||||
|
||||
RideFile *ride = rideItem->ride;
|
||||
|
||||
if (ride) {
|
||||
setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
|
||||
|
||||
static const int maxSize = 4096;
|
||||
|
||||
// recording interval in minutes
|
||||
dt = ride->recIntSecs() / 60.0;
|
||||
|
||||
wattsArray.resize(0);
|
||||
nmArray.resize(0);
|
||||
hrArray.resize(0);
|
||||
kphArray.resize(0);
|
||||
cadArray.resize(0);
|
||||
|
||||
QListIterator<RideFilePoint*> j(ride->dataPoints());
|
||||
|
||||
// unit conversion factor for imperial units for selected parameters
|
||||
double torque_factor = (useMetricUnits ? 1.0 : 0.73756215);
|
||||
double speed_factor = (useMetricUnits ? 1.0 : 0.62137119);
|
||||
|
||||
while (j.hasNext()) {
|
||||
const RideFilePoint *p1 = j.next();
|
||||
|
||||
int wattsIndex = int(floor(p1->watts / wattsDelta));
|
||||
if (wattsIndex >= 0 && wattsIndex < maxSize) {
|
||||
if (wattsIndex >= wattsArray.size())
|
||||
wattsArray.resize(wattsIndex + 1);
|
||||
wattsArray[wattsIndex]++;
|
||||
}
|
||||
|
||||
int nmIndex = int(floor(p1->nm * torque_factor / nmDelta));
|
||||
if (nmIndex >= 0 && nmIndex < maxSize) {
|
||||
if (nmIndex >= nmArray.size())
|
||||
nmArray.resize(nmIndex + 1);
|
||||
nmArray[nmIndex]++;
|
||||
}
|
||||
|
||||
int hrIndex = int(floor(p1->hr / hrDelta));
|
||||
if (hrIndex >= 0 && hrIndex < maxSize) {
|
||||
if (hrIndex >= hrArray.size())
|
||||
hrArray.resize(hrIndex + 1);
|
||||
hrArray[hrIndex]++;
|
||||
}
|
||||
|
||||
int kphIndex = int(floor(p1->kph * speed_factor / kphDelta));
|
||||
if (kphIndex >= 0 && kphIndex < maxSize) {
|
||||
if (kphIndex >= kphArray.size())
|
||||
kphArray.resize(kphIndex + 1);
|
||||
kphArray[kphIndex]++;
|
||||
}
|
||||
|
||||
int cadIndex = int(floor(p1->cad / cadDelta));
|
||||
if (cadIndex >= 0 && cadIndex < maxSize) {
|
||||
if (cadIndex >= cadArray.size())
|
||||
cadArray.resize(cadIndex + 1);
|
||||
cadArray[cadIndex]++;
|
||||
}
|
||||
}
|
||||
|
||||
recalc();
|
||||
}
|
||||
else {
|
||||
setTitle("no data");
|
||||
replot();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setBinWidth(int value)
|
||||
{
|
||||
binw = value;
|
||||
recalc();
|
||||
}
|
||||
|
||||
double
|
||||
PowerHist::getDelta()
|
||||
{
|
||||
switch (selected) {
|
||||
case wattsShaded:
|
||||
case wattsUnshaded:
|
||||
return wattsDelta;
|
||||
case nm:
|
||||
return nmDelta;
|
||||
case hr:
|
||||
return hrDelta;
|
||||
case kph:
|
||||
return kphDelta;
|
||||
case cad:
|
||||
return cadDelta;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
PowerHist::getDigits()
|
||||
{
|
||||
switch (selected) {
|
||||
case wattsShaded:
|
||||
case wattsUnshaded:
|
||||
return wattsDigits;
|
||||
case nm:
|
||||
return nmDigits;
|
||||
case hr:
|
||||
return hrDigits;
|
||||
case kph:
|
||||
return kphDigits;
|
||||
case cad:
|
||||
return cadDigits;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
PowerHist::setBinWidthRealUnits(double value)
|
||||
{
|
||||
setBinWidth(round(value / getDelta()));
|
||||
return binw;
|
||||
}
|
||||
|
||||
double
|
||||
PowerHist::getBinWidthRealUnits()
|
||||
{
|
||||
return binw * getDelta();
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setWithZeros(bool value)
|
||||
{
|
||||
withz = value;
|
||||
recalc();
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setlnY(bool value)
|
||||
{
|
||||
// note: setAxisScaleEngine deletes the old ScaleEngine, so specifying
|
||||
// "new" in the argument list is not a leak
|
||||
|
||||
lny=value;
|
||||
if (lny)
|
||||
{
|
||||
setAxisScaleEngine(yLeft, new QwtLog10ScaleEngine);
|
||||
curve->setBaseline(1e-6);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAxisScaleEngine(yLeft, new QwtLinearScaleEngine);
|
||||
curve->setBaseline(0);
|
||||
}
|
||||
setYMax();
|
||||
replot();
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setParameterAxisTitle()
|
||||
{
|
||||
setAxisTitle(
|
||||
xBottom,
|
||||
((selected == wattsShaded) ||
|
||||
(selected == wattsUnshaded)
|
||||
) ?
|
||||
"watts" :
|
||||
((selected == hr) ?
|
||||
"beats/minute" :
|
||||
((selected == cad) ?
|
||||
"revolutions/min" :
|
||||
useMetricUnits ?
|
||||
((selected == nm) ?
|
||||
"newton-meters" :
|
||||
((selected == kph) ?
|
||||
"km/hr" :
|
||||
"undefined"
|
||||
)
|
||||
) :
|
||||
((selected == nm) ?
|
||||
"ft-lb" :
|
||||
((selected == kph) ?
|
||||
"miles/hr" :
|
||||
"undefined"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
PowerHist::setSelection(Selection selection) {
|
||||
if (selected == selection)
|
||||
return;
|
||||
|
||||
selected = selection;
|
||||
setParameterAxisTitle();
|
||||
recalc();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
PowerHist::fixSelection() {
|
||||
|
||||
Selection s = selected;
|
||||
RideFile *ride = rideItem->ride;
|
||||
|
||||
if (ride)
|
||||
do
|
||||
{
|
||||
if ((s == wattsShaded) || (s == wattsUnshaded))
|
||||
{
|
||||
if (ride->areDataPresent()->watts)
|
||||
setSelection(s);
|
||||
else
|
||||
s = nm;
|
||||
}
|
||||
|
||||
else if (s == nm)
|
||||
{
|
||||
if (ride->areDataPresent()->nm)
|
||||
setSelection(s);
|
||||
else
|
||||
s = hr;
|
||||
}
|
||||
|
||||
else if (s == hr)
|
||||
{
|
||||
if (ride->areDataPresent()->hr)
|
||||
setSelection(s);
|
||||
else
|
||||
s = kph;
|
||||
}
|
||||
|
||||
else if (s == kph)
|
||||
{
|
||||
if (ride->areDataPresent()->kph)
|
||||
setSelection(s);
|
||||
else
|
||||
s = cad;
|
||||
}
|
||||
|
||||
else if (s == cad)
|
||||
{
|
||||
if (ride->areDataPresent()->cad)
|
||||
setSelection(s);
|
||||
else
|
||||
s = wattsShaded;
|
||||
}
|
||||
} while (s != selected);
|
||||
}
|
||||
|
||||
|
||||
bool PowerHist::shadeZones() const
|
||||
{
|
||||
return (
|
||||
rideItem &&
|
||||
rideItem->ride &&
|
||||
selected == wattsShaded
|
||||
);
|
||||
}
|
||||
126
src/PowerHist.h
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_PowerHist_h
|
||||
#define _GC_PowerHist_h 1
|
||||
|
||||
#include <qwt_plot.h>
|
||||
#include <qsettings.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
class QwtPlotCurve;
|
||||
class QwtPlotGrid;
|
||||
class RideItem;
|
||||
class PowerHistBackground;
|
||||
class PowerHistZoneLabel;
|
||||
|
||||
class PowerHist : public QwtPlot
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
QwtPlotCurve *curve;
|
||||
QList <PowerHistZoneLabel *> zoneLabels;
|
||||
|
||||
PowerHist();
|
||||
~PowerHist();
|
||||
|
||||
int binWidth() const { return binw; }
|
||||
inline bool islnY() const { return lny; }
|
||||
inline bool withZeros() const { return withz; }
|
||||
bool shadeZones() const;
|
||||
|
||||
enum Selection {
|
||||
wattsShaded,
|
||||
wattsUnshaded,
|
||||
nm,
|
||||
hr,
|
||||
kph,
|
||||
cad
|
||||
} selected;
|
||||
inline Selection selection() { return selected; }
|
||||
|
||||
void setData(RideItem *_rideItem);
|
||||
|
||||
void setSelection(Selection selection);
|
||||
void fixSelection();
|
||||
|
||||
void setBinWidth(int value);
|
||||
double getDelta();
|
||||
int getDigits();
|
||||
double getBinWidthRealUnits();
|
||||
int setBinWidthRealUnits(double value);
|
||||
|
||||
void refreshZoneLabels();
|
||||
|
||||
RideItem *rideItem;
|
||||
|
||||
public slots:
|
||||
|
||||
void setlnY(bool value);
|
||||
void setWithZeros(bool value);
|
||||
|
||||
protected:
|
||||
|
||||
QwtPlotGrid *grid;
|
||||
|
||||
// storage for data counts
|
||||
QVector<unsigned int>
|
||||
wattsArray,
|
||||
nmArray,
|
||||
hrArray,
|
||||
kphArray,
|
||||
cadArray;
|
||||
|
||||
int binw;
|
||||
|
||||
bool withz; // whether zeros are included in histogram
|
||||
double dt; // length of sample
|
||||
|
||||
void recalc();
|
||||
void setYMax();
|
||||
|
||||
private:
|
||||
QSettings *settings;
|
||||
QVariant unit;
|
||||
|
||||
PowerHistBackground *bg;
|
||||
bool lny;
|
||||
|
||||
// discritized unit for smoothing
|
||||
static const double wattsDelta = 1.0;
|
||||
static const double nmDelta = 0.1;
|
||||
static const double hrDelta = 1.0;
|
||||
static const double kphDelta = 0.1;
|
||||
static const double cadDelta = 1.0;
|
||||
|
||||
// digits for text entry validator
|
||||
static const int wattsDigits = 0;
|
||||
static const int nmDigits = 1;
|
||||
static const int hrDigits = 0;
|
||||
static const int kphDigits = 1;
|
||||
static const int cadDigits = 0;
|
||||
|
||||
void setParameterAxisTitle();
|
||||
|
||||
|
||||
bool useMetricUnits; // whether metric units are used (or imperial)
|
||||
};
|
||||
|
||||
#endif // _GC_PowerHist_h
|
||||
327
src/PowerTapDevice.cpp
Normal file
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "PowerTapDevice.h"
|
||||
#include "PowerTapUtil.h"
|
||||
#include <math.h>
|
||||
|
||||
#define PT_DEBUG false
|
||||
|
||||
static bool powerTapRegistered =
|
||||
Device::addDevice("PowerTap", new PowerTapDevice());
|
||||
|
||||
QString
|
||||
PowerTapDevice::downloadInstructions() const
|
||||
{
|
||||
return ("Make sure the PowerTap unit is turned\n"
|
||||
"on and that its display says, \"Host\"");
|
||||
}
|
||||
|
||||
static bool
|
||||
hasNewline(const char *buf, int len)
|
||||
{
|
||||
static char newline[] = { 0x0d, 0x0a };
|
||||
if (len < 2)
|
||||
return false;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
bool success = true;
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
if (buf[i+j] != newline[j]) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static QString
|
||||
cEscape(const char *buf, int len)
|
||||
{
|
||||
char *result = new char[4 * len + 1];
|
||||
char *tmp = result;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
if (buf[i] == '"')
|
||||
tmp += sprintf(tmp, "\\\"");
|
||||
else if (isprint(buf[i]))
|
||||
*(tmp++) = buf[i];
|
||||
else
|
||||
tmp += sprintf(tmp, "\\x%02x", 0xff & (unsigned) buf[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
doWrite(CommPortPtr dev, char c, bool hwecho, QString &err)
|
||||
{
|
||||
if (PT_DEBUG) printf("writing '%c' to device\n", c);
|
||||
int n = dev->write(&c, 1, err);
|
||||
if (n != 1) {
|
||||
if (n < 0)
|
||||
err = QString("failed to write %1 to device: %2").arg(c).arg(err);
|
||||
else
|
||||
err = QString("timeout writing %1 to device").arg(c);
|
||||
return false;
|
||||
}
|
||||
if (hwecho) {
|
||||
char c;
|
||||
int n = dev->read(&c, 1, err);
|
||||
if (n != 1) {
|
||||
if (n < 0)
|
||||
err = QString("failed to read back hardware echo: %2").arg(err);
|
||||
else
|
||||
err = "timeout reading back hardware echo";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
readUntilNewline(CommPortPtr dev, char *buf, int len, QString &err)
|
||||
{
|
||||
int sofar = 0;
|
||||
while (!hasNewline(buf, sofar)) {
|
||||
assert(sofar < len);
|
||||
// Read one byte at a time to avoid waiting for timeout.
|
||||
int n = dev->read(buf + sofar, 1, err);
|
||||
if (n <= 0) {
|
||||
err = (n < 0) ? ("read error: " + err) : "read timeout";
|
||||
err += QString(", read %1 bytes so far: \"%2\"")
|
||||
.arg(sofar).arg(cEscape(buf, sofar));
|
||||
return -1;
|
||||
}
|
||||
sofar += n;
|
||||
}
|
||||
return sofar;
|
||||
}
|
||||
|
||||
bool
|
||||
PowerTapDevice::download(CommPortPtr dev, const QDir &tmpdir,
|
||||
QString &tmpname, QString &filename,
|
||||
StatusCallback statusCallback, QString &err)
|
||||
{
|
||||
if (!dev->open(err)) {
|
||||
err = "ERROR: open failed: " + err;
|
||||
return false;
|
||||
}
|
||||
// make several attempts at reading the version
|
||||
int attempts = 3;
|
||||
QString cbtext;
|
||||
int veridx = -1;
|
||||
int version_len;
|
||||
char vbuf[256];
|
||||
QByteArray version;
|
||||
|
||||
do {
|
||||
if (!doWrite(dev, 0x56, false, err)) // 'V'
|
||||
return false;
|
||||
|
||||
cbtext = "Reading version...";
|
||||
if (!statusCallback(cbtext)) {
|
||||
err = "download cancelled";
|
||||
return false;
|
||||
}
|
||||
|
||||
version_len = readUntilNewline(dev, vbuf, sizeof(vbuf), err);
|
||||
if (version_len < 0) {
|
||||
err = "Error reading version: " + err;
|
||||
return false;
|
||||
}
|
||||
if (PT_DEBUG) {
|
||||
printf("read version \"%s\"\n",
|
||||
cEscape(vbuf, version_len).toAscii().constData());
|
||||
}
|
||||
version = QByteArray(vbuf, version_len);
|
||||
|
||||
// We expect the version string to be something like
|
||||
// "VER 02.21 PRO...", so if we see two V's, it's probably
|
||||
// because there's a hardware echo going on.
|
||||
veridx = version.indexOf("VER");
|
||||
|
||||
} while ((--attempts > 0) && (veridx < 0));
|
||||
|
||||
if (veridx < 0) {
|
||||
err = QString("Unrecognized version \"%1\"")
|
||||
.arg(cEscape(vbuf, version_len));
|
||||
return false;
|
||||
}
|
||||
bool hwecho = version.indexOf('V') < veridx;
|
||||
if (PT_DEBUG) printf("hwecho=%s\n", hwecho ? "true" : "false");
|
||||
|
||||
cbtext += "done.\nReading header...";
|
||||
if (!statusCallback(cbtext)) {
|
||||
err = "download cancelled";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!doWrite(dev, 0x44, hwecho, err)) // 'D'
|
||||
return false;
|
||||
unsigned char header[6];
|
||||
int header_len = dev->read(header, sizeof(header), err);
|
||||
if (header_len != 6) {
|
||||
if (header_len < 0)
|
||||
err = "ERROR: reading header: " + err;
|
||||
else
|
||||
err = "ERROR: timeout reading header";
|
||||
return false;
|
||||
}
|
||||
if (PT_DEBUG) {
|
||||
printf("read header \"%s\"\n",
|
||||
cEscape((char*) header,
|
||||
sizeof(header)).toAscii().constData());
|
||||
}
|
||||
QVector<unsigned char> records;
|
||||
for (size_t i = 0; i < sizeof(header); ++i)
|
||||
records.append(header[i]);
|
||||
|
||||
cbtext += "done.\nReading ride data...\n";
|
||||
if (!statusCallback(cbtext)) {
|
||||
err = "download cancelled";
|
||||
return false;
|
||||
}
|
||||
int cbtextlen = cbtext.length();
|
||||
double recIntSecs = 0.0;
|
||||
|
||||
fflush(stdout);
|
||||
while (true) {
|
||||
if (PT_DEBUG) printf("reading block\n");
|
||||
unsigned char buf[256 * 6 + 1];
|
||||
int n = dev->read(buf, 2, err);
|
||||
if (n < 2) {
|
||||
if (n < 0)
|
||||
err = "ERROR: reading first two: " + err;
|
||||
else
|
||||
err = "ERROR: timeout reading first two";
|
||||
return false;
|
||||
}
|
||||
if (PT_DEBUG) {
|
||||
printf("read 2 bytes: \"%s\"\n",
|
||||
cEscape((char*) buf, 2).toAscii().constData());
|
||||
}
|
||||
if (hasNewline((char*) buf, 2))
|
||||
break;
|
||||
unsigned count = 2;
|
||||
while (count < sizeof(buf)) {
|
||||
n = dev->read(buf + count, sizeof(buf) - count, err);
|
||||
if (n < 0) {
|
||||
err = "ERROR: reading block: " + err;
|
||||
return false;
|
||||
}
|
||||
if (n == 0) {
|
||||
err = "ERROR: timeout reading block";
|
||||
return false;
|
||||
}
|
||||
if (PT_DEBUG) {
|
||||
printf("read %d bytes: \"%s\"\n", n,
|
||||
cEscape((char*) buf + count, n).toAscii().constData());
|
||||
}
|
||||
count += n;
|
||||
}
|
||||
unsigned csum = 0;
|
||||
for (int i = 0; i < ((int) sizeof(buf)) - 1; ++i)
|
||||
csum += buf[i];
|
||||
if ((csum % 256) != buf[sizeof(buf) - 1]) {
|
||||
err = "ERROR: bad checksum";
|
||||
return false;
|
||||
}
|
||||
if (PT_DEBUG) printf("good checksum\n");
|
||||
for (size_t i = 0; i < sizeof(buf) - 1; ++i)
|
||||
records.append(buf[i]);
|
||||
if (recIntSecs == 0.0) {
|
||||
unsigned char *data = records.data();
|
||||
bool bIsVer81 = PowerTapUtil::is_Ver81(data);
|
||||
for (int i = 0; i < records.size(); i += 6) {
|
||||
if (PowerTapUtil::is_config(data + i, bIsVer81)) {
|
||||
unsigned unused1, unused2, unused3;
|
||||
PowerTapUtil::unpack_config(
|
||||
data + i, &unused1, &unused2,
|
||||
&recIntSecs, &unused3, bIsVer81);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (recIntSecs != 0.0) {
|
||||
int min = (int) round(records.size() / 6 * recIntSecs);
|
||||
cbtext.chop(cbtext.size() - cbtextlen);
|
||||
cbtext.append(QString("Ride data read: %1:%2").arg(min / 60)
|
||||
.arg(min % 60, 2, 10, QLatin1Char('0')));
|
||||
}
|
||||
if (!statusCallback(cbtext)) {
|
||||
err = "download cancelled";
|
||||
return false;
|
||||
}
|
||||
if (!doWrite(dev, 0x71, hwecho, err)) // 'q'
|
||||
return false;
|
||||
}
|
||||
|
||||
QString tmpl = tmpdir.absoluteFilePath(".ptdl.XXXXXX");
|
||||
QTemporaryFile tmp(tmpl);
|
||||
tmp.setAutoRemove(false);
|
||||
if (!tmp.open()) {
|
||||
err = "Failed to create temporary file "
|
||||
+ tmpl + ": " + tmp.error();
|
||||
return false;
|
||||
}
|
||||
QTextStream os(&tmp);
|
||||
os << hex;
|
||||
os.setPadChar('0');
|
||||
|
||||
struct tm time;
|
||||
bool time_set = false;
|
||||
unsigned char *data = records.data();
|
||||
bool bIsVer81 = PowerTapUtil::is_Ver81(data);
|
||||
|
||||
for (int i = 0; i < records.size(); i += 6) {
|
||||
if (data[i] == 0 && !bIsVer81)
|
||||
continue;
|
||||
for (int j = 0; j < 6; ++j) {
|
||||
os.setFieldWidth(2);
|
||||
os << data[i+j];
|
||||
os.setFieldWidth(1);
|
||||
os << ((j == 5) ? "\n" : " ");
|
||||
}
|
||||
if (!time_set && PowerTapUtil::is_time(data + i, bIsVer81)) {
|
||||
PowerTapUtil::unpack_time(data + i, &time, bIsVer81);
|
||||
time_set = true;
|
||||
}
|
||||
}
|
||||
if (!time_set) {
|
||||
err = "Failed to find ride time.";
|
||||
tmp.setAutoRemove(true);
|
||||
return false;
|
||||
}
|
||||
tmpname = tmp.fileName(); // after close(), tmp.fileName() is ""
|
||||
tmp.close();
|
||||
// QTemporaryFile initially has permissions set to 0600.
|
||||
// Make it readable by everyone.
|
||||
tmp.setPermissions(tmp.permissions()
|
||||
| QFile::ReadOwner | QFile::ReadUser
|
||||
| QFile::ReadGroup | QFile::ReadOther);
|
||||
|
||||
char filename_tmp[32];
|
||||
sprintf(filename_tmp, "%04d_%02d_%02d_%02d_%02d_%02d.raw",
|
||||
time.tm_year + 1900, time.tm_mon + 1,
|
||||
time.tm_mday, time.tm_hour, time.tm_min,
|
||||
time.tm_sec);
|
||||
filename = filename_tmp;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
34
src/PowerTapDevice.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_PowerTapDevice_h
|
||||
#define _GC_PowerTapDevice_h 1
|
||||
|
||||
#include "CommPort.h"
|
||||
#include "Device.h"
|
||||
|
||||
struct PowerTapDevice : public Device
|
||||
{
|
||||
virtual QString downloadInstructions() const;
|
||||
virtual bool download(CommPortPtr dev, const QDir &tmpdir,
|
||||
QString &tmpname, QString &filename,
|
||||
StatusCallback statusCallback, QString &err);
|
||||
};
|
||||
|
||||
#endif // _GC_PowerTapDevice_h
|
||||
|
||||
219
src/PowerTapUtil.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "PowerTapUtil.h"
|
||||
#include <QString>
|
||||
#include <math.h>
|
||||
|
||||
bool
|
||||
PowerTapUtil::is_ignore_record(unsigned char *buf, bool bVer81)
|
||||
{
|
||||
if (bVer81)
|
||||
return buf[0]==0 && buf[1]==0 && buf[2]==0;
|
||||
else
|
||||
return buf[0]==0;
|
||||
}
|
||||
|
||||
bool
|
||||
PowerTapUtil::is_Ver81(unsigned char *buf)
|
||||
{
|
||||
return buf[3] == 0x81;
|
||||
}
|
||||
|
||||
int
|
||||
PowerTapUtil::is_time(unsigned char *buf, bool bVer81)
|
||||
{
|
||||
return (bVer81 && buf[0] == 0x10) || (!bVer81 && buf[0] == 0x60);
|
||||
}
|
||||
|
||||
time_t
|
||||
PowerTapUtil::unpack_time(unsigned char *buf, struct tm *time, bool bVer81)
|
||||
{
|
||||
(void) bVer81; // unused
|
||||
memset(time, 0, sizeof(*time));
|
||||
time->tm_year = 2000 + buf[1] - 1900;
|
||||
time->tm_mon = buf[2] - 1;
|
||||
time->tm_mday = buf[3] & 0x1f;
|
||||
time->tm_hour = buf[4] & 0x1f;
|
||||
time->tm_min = buf[5] & 0x3f;
|
||||
time->tm_sec = ((buf[3] >> 5) << 3) | (buf[4] >> 5);
|
||||
time->tm_isdst = -1;
|
||||
return mktime(time);
|
||||
}
|
||||
|
||||
int
|
||||
PowerTapUtil::is_config(unsigned char *buf, bool bVer81)
|
||||
{
|
||||
return (bVer81 && buf[0] == 0x00) || (!bVer81 && buf[0] == 0x40);
|
||||
}
|
||||
|
||||
const double TIME_UNIT_SEC = 0.021*60.0;
|
||||
const double TIME_UNIT_SEC_V81 = 0.01;
|
||||
|
||||
int
|
||||
PowerTapUtil::unpack_config(unsigned char *buf, unsigned *interval,
|
||||
unsigned *last_interval, double *rec_int_secs,
|
||||
unsigned *wheel_sz_mm, bool bVer81)
|
||||
{
|
||||
*wheel_sz_mm = (buf[1] << 8) | buf[2];
|
||||
/* Data from device wraps interval after 9... */
|
||||
if (buf[3] != *last_interval) {
|
||||
*last_interval = buf[3];
|
||||
++*interval;
|
||||
}
|
||||
|
||||
*rec_int_secs = buf[4];
|
||||
if (bVer81)
|
||||
{
|
||||
*rec_int_secs *= TIME_UNIT_SEC_V81;
|
||||
}
|
||||
else
|
||||
{
|
||||
*rec_int_secs += 1;
|
||||
*rec_int_secs *= TIME_UNIT_SEC;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
PowerTapUtil::is_data(unsigned char *buf, bool bVer81)
|
||||
{
|
||||
if (bVer81)
|
||||
return (buf[0] & 0x40) == 0x40;
|
||||
else
|
||||
return (buf[0] & 0x80) == 0x80;
|
||||
}
|
||||
|
||||
static double
|
||||
my_round(double x)
|
||||
{
|
||||
int i = (int) x;
|
||||
double z = x - i;
|
||||
/* For some unknown reason, the PowerTap software rounds 196.5 down... */
|
||||
if ((z > 0.5) || ((z == 0.5) && (i != 196)))
|
||||
++i;
|
||||
return i;
|
||||
}
|
||||
|
||||
#define MAGIC_CONSTANT 147375.0
|
||||
#define PI M_PI
|
||||
|
||||
#define LBFIN_TO_NM 0.11298483
|
||||
#define KM_TO_MI 0.62137119
|
||||
|
||||
#define BAD_LBFIN_TO_NM_1 0.112984
|
||||
#define BAD_LBFIN_TO_NM_2 0.1129824
|
||||
#define BAD_KM_TO_MI 0.62
|
||||
|
||||
void
|
||||
PowerTapUtil::unpack_data(unsigned char *buf, int compat, double rec_int_secs,
|
||||
unsigned wheel_sz_mm, double *time_secs,
|
||||
double *torque_Nm, double *mph, double *watts,
|
||||
double *dist_m, unsigned *cad, unsigned *hr,
|
||||
bool bVer81)
|
||||
{
|
||||
if (bVer81)
|
||||
{
|
||||
const double CLOCK_TICK_TIME = 0.000512;
|
||||
const double METERS_PER_SEC_TO_MPH = 2.23693629;
|
||||
|
||||
*time_secs += rec_int_secs;
|
||||
int rotations = buf[0] & 0x0f;
|
||||
int ticks_for_1_rotation = (buf[1]<<4) | (buf[2]>>4);
|
||||
|
||||
if (ticks_for_1_rotation==0xff0 || ticks_for_1_rotation==0)
|
||||
{
|
||||
*watts = 0;
|
||||
*cad = 0;
|
||||
*mph = 0;
|
||||
*torque_Nm = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
*watts = ((buf[2] & 0x0f) << 8) | buf[3];
|
||||
*cad = buf[4];
|
||||
if (*cad == 0xff)
|
||||
*cad = 0;
|
||||
|
||||
double wheel_sz_meters = wheel_sz_mm / 1000.0;
|
||||
*dist_m += rotations * wheel_sz_meters;
|
||||
double seconds_for_1_rotation = ticks_for_1_rotation * CLOCK_TICK_TIME;
|
||||
double meters_per_sec = wheel_sz_meters / seconds_for_1_rotation;
|
||||
*mph = meters_per_sec * METERS_PER_SEC_TO_MPH;
|
||||
|
||||
*torque_Nm = (*watts * seconds_for_1_rotation)/(2.0*PI);
|
||||
}
|
||||
*hr = buf[5];
|
||||
if (*hr == 0xff)
|
||||
*hr = 0;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
double kph10;
|
||||
unsigned speed;
|
||||
unsigned torque_inlbs;
|
||||
|
||||
*time_secs += rec_int_secs;
|
||||
torque_inlbs = ((buf[1] & 0xf0) << 4) | buf[2];
|
||||
if (torque_inlbs == 0xfff)
|
||||
torque_inlbs = 0;
|
||||
speed = ((buf[1] & 0x0f) << 8) | buf[3];
|
||||
if ((speed < 100) || (speed == 0xfff)) {
|
||||
if ((speed != 0) && (speed < 1000)) {
|
||||
fprintf(stderr, "possible error: speed=%.1f; ignoring it\n",
|
||||
MAGIC_CONSTANT / speed / 10.0);
|
||||
}
|
||||
*mph = -1.0;
|
||||
*watts = -1.0;
|
||||
}
|
||||
else {
|
||||
if (compat)
|
||||
*torque_Nm = torque_inlbs * BAD_LBFIN_TO_NM_2;
|
||||
else
|
||||
*torque_Nm = torque_inlbs * LBFIN_TO_NM;
|
||||
kph10 = MAGIC_CONSTANT / speed;
|
||||
if (compat)
|
||||
*mph = my_round(kph10) / 10.0 * BAD_KM_TO_MI;
|
||||
else
|
||||
*mph = kph10 / 10.0 * KM_TO_MI;
|
||||
|
||||
// from http://en.wikipedia.org/wiki/Torque#Conversion_to_other_units
|
||||
double dMetersPerMinute = (kph10 / 10.0) * 1000.0 / 60.0;
|
||||
double dWheelSizeMeters = wheel_sz_mm / 1000.0;
|
||||
double rpm = dMetersPerMinute/dWheelSizeMeters;
|
||||
*watts = *torque_Nm * rpm * 2.0 * PI /60.0;
|
||||
|
||||
if (compat)
|
||||
*watts = my_round(*watts);
|
||||
else
|
||||
*watts = round(*watts);
|
||||
}
|
||||
if (compat)
|
||||
*torque_Nm = torque_inlbs * BAD_LBFIN_TO_NM_1;
|
||||
*dist_m += (buf[0] & 0x7f) * wheel_sz_mm / 1000.0;
|
||||
*cad = buf[4];
|
||||
if (*cad == 0xff)
|
||||
*cad = 0;
|
||||
*hr = buf[5];
|
||||
if (*hr == 0xff)
|
||||
*hr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
src/PowerTapUtil.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_PowerTapUtil_h
|
||||
#define _GC_PowerTapUtil_h 1
|
||||
|
||||
#include <time.h>
|
||||
|
||||
struct PowerTapUtil
|
||||
{
|
||||
static bool is_Ver81(unsigned char *bufHeader);
|
||||
|
||||
static bool is_ignore_record(unsigned char *buf, bool bVer81);
|
||||
|
||||
static int is_time(unsigned char *buf, bool bVer81);
|
||||
static time_t unpack_time(unsigned char *buf, struct tm *time, bool bVer81);
|
||||
|
||||
static int is_config(unsigned char *buf, bool bVer81);
|
||||
static int unpack_config(unsigned char *buf, unsigned *interval,
|
||||
unsigned *last_interval, double *rec_int_secs,
|
||||
unsigned *wheel_sz_mm, bool bVer81);
|
||||
|
||||
static int is_data(unsigned char *buf, bool bVer81);
|
||||
static void unpack_data(unsigned char *buf, int compat, double rec_int_secs,
|
||||
unsigned wheel_sz_mm, double *time_secs,
|
||||
double *torque_Nm, double *mph, double *watts,
|
||||
double *dist_m, unsigned *cad, unsigned *hr, bool bVer81);
|
||||
};
|
||||
|
||||
#endif // _GC_PowerTapUtil_h
|
||||
|
||||
128
src/QuarqParser.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include <QString>
|
||||
#include <iostream>
|
||||
#include <assert.h>
|
||||
#include "QuarqParser.h"
|
||||
|
||||
QuarqParser::QuarqParser (RideFile* rideFile)
|
||||
: rideFile(rideFile),
|
||||
version(""),
|
||||
km(0),
|
||||
watts(0),
|
||||
cad(0),
|
||||
hr(0),
|
||||
initial_seconds(-1.0),
|
||||
seconds_from_start(0),
|
||||
kph(0),
|
||||
nm(0)
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
// implement sample-and-hold resampling
|
||||
|
||||
#define SAMPLE_INTERVAL 1.0 // seconds
|
||||
|
||||
void
|
||||
QuarqParser::incrementTime( const double new_time )
|
||||
{
|
||||
if (initial_seconds < 0.0) {
|
||||
initial_seconds = new_time;
|
||||
}
|
||||
|
||||
float time_diff = new_time - initial_seconds;
|
||||
|
||||
while (time_diff > seconds_from_start) {
|
||||
|
||||
rideFile->appendPoint(seconds_from_start, cad, hr, km,
|
||||
kph, nm, watts, 0, 9, 0);
|
||||
|
||||
seconds_from_start += SAMPLE_INTERVAL;
|
||||
}
|
||||
time.setTime_t(new_time);
|
||||
}
|
||||
|
||||
bool
|
||||
QuarqParser::startElement( const QString&, const QString&,
|
||||
const QString& qName,
|
||||
const QXmlAttributes& qAttributes)
|
||||
{
|
||||
buf.clear();
|
||||
|
||||
if (qName == "Qollector") {
|
||||
version = qAttributes.value("version");
|
||||
|
||||
// reset the timer for a new <Qollector> tag
|
||||
seconds_from_start = 0.0;
|
||||
initial_seconds = -1;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#define CheckQuarqXml(name,unit,dest) do { \
|
||||
if (qName== #name) { \
|
||||
QString name = qAttributes.value( #unit ); \
|
||||
QString timestamp = qAttributes.value("timestamp"); \
|
||||
\
|
||||
if ((! name.isEmpty()) && (!timestamp.isEmpty()) && \
|
||||
( name.toLower() != "nan")) { \
|
||||
dest = name.toDouble(); \
|
||||
incrementTime(timestamp.toDouble()); \
|
||||
} \
|
||||
return TRUE; \
|
||||
} \
|
||||
} while (0);
|
||||
|
||||
CheckQuarqXml(Cadence, RPM, cad );
|
||||
CheckQuarqXml(Power, Watts, watts );
|
||||
CheckQuarqXml(HeartRate, BPM, hr );
|
||||
// clearly bogus, equating RPM to kph.
|
||||
// Unless you have an 18 foot wheel, by chance
|
||||
CheckQuarqXml(Speed, RPM, kph );
|
||||
|
||||
#undef CheckQuarqXml
|
||||
|
||||
// default case
|
||||
|
||||
// only print the first time and unknown happens
|
||||
if (!unknown_keys[qName]++)
|
||||
std::cerr << "Unknown Element " << qPrintable(qName) << std::endl;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool
|
||||
QuarqParser::endElement( const QString&, const QString&, const QString& qName)
|
||||
{
|
||||
|
||||
// flush one last data point
|
||||
if (qName == "Qollector") {
|
||||
rideFile->appendPoint(seconds_from_start, cad, hr, km,
|
||||
kph, nm, watts, 0, 0, 0);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
bool
|
||||
QuarqParser::characters( const QString& str )
|
||||
{
|
||||
buf += str;
|
||||
return TRUE;
|
||||
}
|
||||
68
src/QuarqParser.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _QuarqParser_h
|
||||
#define _QuarqParser_h
|
||||
|
||||
#include "RideFile.h"
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
#include <QDateTime>
|
||||
#include <QProcess>
|
||||
#include <QXmlDefaultHandler>
|
||||
|
||||
class QuarqParser : public QXmlDefaultHandler
|
||||
{
|
||||
public:
|
||||
QuarqParser(RideFile* rideFile);
|
||||
|
||||
bool startElement( const QString&, const QString&, const QString&,
|
||||
const QXmlAttributes& );
|
||||
bool endElement( const QString&, const QString&, const QString& );
|
||||
|
||||
bool characters( const QString& );
|
||||
|
||||
private:
|
||||
|
||||
void incrementTime( const double new_time ) ;
|
||||
|
||||
RideFile* rideFile;
|
||||
|
||||
QString buf;
|
||||
|
||||
QString version;
|
||||
|
||||
QDateTime time;
|
||||
double km;
|
||||
|
||||
double watts;
|
||||
double cad;
|
||||
double hr;
|
||||
|
||||
double initial_seconds;
|
||||
double seconds_from_start;
|
||||
|
||||
double kph;
|
||||
double nm;
|
||||
|
||||
QHash<QString, int> unknown_keys;
|
||||
|
||||
};
|
||||
|
||||
#endif // _QuarqParser_h
|
||||
|
||||
153
src/QuarqRideFile.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "QuarqRideFile.h"
|
||||
#include "QuarqParser.h"
|
||||
#include <iostream>
|
||||
#include <assert.h>
|
||||
|
||||
static QString installed_path = "";
|
||||
|
||||
|
||||
QProcess *getInterpreterProcess( QString path ) {
|
||||
|
||||
QProcess *antProcess;
|
||||
|
||||
antProcess = new QProcess( );
|
||||
antProcess->start( path );
|
||||
|
||||
if (!antProcess->waitForStarted()) {
|
||||
delete antProcess;
|
||||
antProcess = NULL;
|
||||
}
|
||||
|
||||
return antProcess;
|
||||
}
|
||||
|
||||
/*
|
||||
Thanks to ANT+ nondisclosure agreements, the Quarq ANT+
|
||||
interpretation code lives in a closed source binary. It takes the
|
||||
log on stdin and writes XML to stdout.
|
||||
|
||||
If the binary is not available, no Quarq ANT+ capability is shown
|
||||
in the menu, nor are any ANT+ files opened.
|
||||
|
||||
QProcess note:
|
||||
|
||||
It turns out that the interpreter must actually be opened and
|
||||
executed before we can be sure it's there. Checking the return
|
||||
value of start() isn't sufficient. On my Linux system, start()
|
||||
returns true upon opening the OS X build of qollector_interpret.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
bool quarqInterpreterInstalled( void ) {
|
||||
|
||||
static bool checkedInstallation = false;
|
||||
static bool installed;
|
||||
|
||||
if (!checkedInstallation) {
|
||||
|
||||
QString interpreterPath="/usr/local/bin/qollector_interpret/build-";
|
||||
QStringList executables;
|
||||
executables << "linux-i386/qollector_interpret";
|
||||
executables << "osx-ppc-i386/qollector_interpret";
|
||||
executables << "win32/qollector_interpret.exe";
|
||||
|
||||
for ( QStringList::Iterator ex = executables.begin(); ex != executables.end(); ++ex ) {
|
||||
|
||||
QProcess *antProcess = getInterpreterProcess( interpreterPath + *ex );
|
||||
|
||||
if (NULL == antProcess) {
|
||||
installed = false;
|
||||
} else {
|
||||
|
||||
antProcess->closeWriteChannel();
|
||||
antProcess->waitForFinished(-1);
|
||||
|
||||
installed=((QProcess::NormalExit == antProcess->exitStatus()) &&
|
||||
(0 == antProcess->exitCode()));
|
||||
|
||||
delete antProcess;
|
||||
|
||||
if (installed) {
|
||||
installed_path = interpreterPath + *ex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!installed)
|
||||
std::cerr << "Cannot open qollector_interpret program, available from http://opensource.quarq.us/qollector_interpret." << std::endl;
|
||||
|
||||
checkedInstallation = true;
|
||||
}
|
||||
|
||||
return installed;
|
||||
}
|
||||
|
||||
static int antFileReaderRegistered =
|
||||
quarqInterpreterInstalled() ? RideFileFactory::instance().registerReader("qla", new QuarqFileReader()) : 0;
|
||||
|
||||
RideFile *QuarqFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
(void) errors;
|
||||
RideFile *rideFile = new RideFile();
|
||||
rideFile->setDeviceType("Quarq Qollector");
|
||||
|
||||
QuarqParser handler(rideFile);
|
||||
|
||||
QProcess *antProcess = getInterpreterProcess( installed_path );
|
||||
|
||||
assert(antProcess);
|
||||
|
||||
QXmlInputSource source (antProcess);
|
||||
QXmlSimpleReader reader;
|
||||
reader.setContentHandler (&handler);
|
||||
|
||||
// this could done be a loop to "save memory."
|
||||
file.open(QIODevice::ReadOnly);
|
||||
antProcess->write(file.readAll());
|
||||
antProcess->closeWriteChannel();
|
||||
antProcess->waitForFinished(-1);
|
||||
|
||||
assert(QProcess::NormalExit == antProcess->exitStatus());
|
||||
assert(0 == antProcess->exitCode());
|
||||
|
||||
reader.parse(source);
|
||||
|
||||
reader.parseContinue();
|
||||
|
||||
QRegExp rideTime("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_"
|
||||
"(\\d\\d)_(\\d\\d)_(\\d\\d)\\.qla$");
|
||||
if (rideTime.indexIn(file.fileName()) >= 0) {
|
||||
QDateTime datetime(QDate(rideTime.cap(1).toInt(),
|
||||
rideTime.cap(2).toInt(),
|
||||
rideTime.cap(3).toInt()),
|
||||
QTime(rideTime.cap(4).toInt(),
|
||||
rideTime.cap(5).toInt(),
|
||||
rideTime.cap(6).toInt()));
|
||||
rideFile->setStartTime(datetime);
|
||||
}
|
||||
|
||||
delete antProcess;
|
||||
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
32
src/QuarqRideFile.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2009 Mark Rages (mark@quarq.us)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _QuarqRideFile_h
|
||||
#define _QuarqRideFile_h
|
||||
|
||||
#include "RideFile.h"
|
||||
#include <QProcess>
|
||||
|
||||
bool quarqInterpreterInstalled( void );
|
||||
|
||||
struct QuarqFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
};
|
||||
|
||||
#endif // _QuarqRideFile_h
|
||||
|
||||
5
src/README.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
To uninstall the older FTDI VCP drivers on Mac OS X, open a Terminal and type:
|
||||
|
||||
sudo mv /System/Library/Extensions/FTDIUSBSerialDriver.kext /tmp
|
||||
|
||||
Type your password when prompted, then restart your computer.
|
||||
217
src/RawRideFile.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (c) 2007-2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RawRideFile.h"
|
||||
#include "PowerTapUtil.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
|
||||
#define MILES_TO_KM 1.609344
|
||||
#define KM_TO_MI 0.62137119
|
||||
#define BAD_KM_TO_MI 0.62
|
||||
|
||||
static int rawFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader("raw", new RawFileReader());
|
||||
|
||||
struct ReadState
|
||||
{
|
||||
RideFile *rideFile;
|
||||
QStringList &errors;
|
||||
double last_secs, last_miles;
|
||||
unsigned last_interval;
|
||||
time_t start_since_epoch;
|
||||
// this seems to not be used
|
||||
//unsigned rec_int;
|
||||
ReadState(RideFile *rideFile,
|
||||
QStringList &errors) :
|
||||
rideFile(rideFile), errors(errors), last_secs(0.0),
|
||||
last_miles(0.0), last_interval(0), start_since_epoch(0)/*, rec_int(0)*/ {}
|
||||
};
|
||||
|
||||
static void
|
||||
config_cb(unsigned interval, double rec_int_secs,
|
||||
unsigned wheel_sz_mm, void *context)
|
||||
{
|
||||
(void) interval;
|
||||
(void) wheel_sz_mm;
|
||||
ReadState *state = (ReadState*) context;
|
||||
// Assume once set, rec_int should never change.
|
||||
//double recIntSecs = rec_int * 1.26;
|
||||
assert((state->rideFile->recIntSecs() == 0.0)
|
||||
|| (state->rideFile->recIntSecs() == rec_int_secs));
|
||||
state->rideFile->setRecIntSecs(rec_int_secs);
|
||||
}
|
||||
|
||||
static void
|
||||
time_cb(struct tm *, time_t since_epoch, void *context)
|
||||
{
|
||||
ReadState *state = (ReadState*) context;
|
||||
if (state->rideFile->startTime().isNull())
|
||||
{
|
||||
QDateTime t;
|
||||
t.setTime_t(since_epoch);
|
||||
state->rideFile->setStartTime(t);
|
||||
}
|
||||
if (state->start_since_epoch == 0)
|
||||
state->start_since_epoch = since_epoch;
|
||||
double secs = since_epoch - state->start_since_epoch;
|
||||
state->rideFile->appendPoint(secs, 0.0, 0.0,
|
||||
state->last_miles * MILES_TO_KM, 0.0,
|
||||
0.0, 0.0, 0.0, state->last_interval);
|
||||
state->last_secs = secs;
|
||||
}
|
||||
|
||||
static void
|
||||
data_cb(double secs, double nm, double mph, double watts, double miles, double alt,
|
||||
unsigned cad, unsigned hr, unsigned interval, void *context)
|
||||
{
|
||||
if (nm < 0.0) nm = 0.0;
|
||||
if (mph < 0.0) mph = 0.0;
|
||||
if (watts < 0.0) watts = 0.0;
|
||||
|
||||
ReadState *state = (ReadState*) context;
|
||||
state->rideFile->appendPoint(secs, cad, hr, miles * MILES_TO_KM,
|
||||
mph * MILES_TO_KM, nm, watts, alt, interval);
|
||||
state->last_secs = secs;
|
||||
state->last_miles = miles;
|
||||
state->last_interval = interval;
|
||||
}
|
||||
|
||||
static void
|
||||
error_cb(const char *msg, void *context)
|
||||
{
|
||||
ReadState *state = (ReadState*) context;
|
||||
state->errors.append(QString(msg));
|
||||
}
|
||||
|
||||
static void
|
||||
pt_read_raw(FILE *in, int compat, void *context,
|
||||
void (*config_cb)(unsigned interval, double rec_int_secs,
|
||||
unsigned wheel_sz_mm, void *context),
|
||||
void (*time_cb)(struct tm *time, time_t since_epoch, void *context),
|
||||
void (*data_cb)(double secs, double nm, double mph, double watts,
|
||||
double miles, double alt, unsigned cad, unsigned hr,
|
||||
unsigned interval, void *context),
|
||||
void (*error_cb)(const char *msg, void *context))
|
||||
{
|
||||
unsigned interval = 0;
|
||||
unsigned last_interval = 0;
|
||||
unsigned wheel_sz_mm = 0;
|
||||
double rec_int_secs = 0.0;
|
||||
int i, n, row = 0;
|
||||
unsigned char buf[6];
|
||||
unsigned sbuf[6];
|
||||
double meters = 0.0;
|
||||
double secs = 0.0, start_secs = 0.0;
|
||||
double miles;
|
||||
double mph;
|
||||
double nm;
|
||||
double watts;
|
||||
double alt;
|
||||
unsigned cad;
|
||||
unsigned hr;
|
||||
struct tm time;
|
||||
time_t since_epoch;
|
||||
char ebuf[256];
|
||||
bool bIsVer81 = false;
|
||||
|
||||
while ((n = fscanf(in, "%x %x %x %x %x %x\n",
|
||||
sbuf, sbuf+1, sbuf+2, sbuf+3, sbuf+4, sbuf+5)) == 6) {
|
||||
++row;
|
||||
for (i = 0; i < 6; ++i) {
|
||||
if (sbuf[i] > 0xff) { n = 1; break; }
|
||||
buf[i] = sbuf[i];
|
||||
}
|
||||
if (row == 1)
|
||||
{
|
||||
/* Serial number? */
|
||||
bIsVer81 = PowerTapUtil::is_Ver81(buf);
|
||||
}
|
||||
else if (PowerTapUtil::is_ignore_record(buf, bIsVer81)) {
|
||||
// do nothing
|
||||
}
|
||||
else if (PowerTapUtil::is_config(buf, bIsVer81)) {
|
||||
if (PowerTapUtil::unpack_config(buf, &interval, &last_interval,
|
||||
&rec_int_secs, &wheel_sz_mm, bIsVer81) < 0) {
|
||||
sprintf(ebuf, "Couldn't unpack config record.");
|
||||
if (error_cb) error_cb(ebuf, context);
|
||||
return;
|
||||
}
|
||||
if (config_cb) config_cb(interval, rec_int_secs, wheel_sz_mm, context);
|
||||
}
|
||||
else if (PowerTapUtil::is_time(buf, bIsVer81)) {
|
||||
since_epoch = PowerTapUtil::unpack_time(buf, &time, bIsVer81);
|
||||
bool ignore = false;
|
||||
if (start_secs == 0.0)
|
||||
start_secs = since_epoch;
|
||||
else if (since_epoch - start_secs > secs)
|
||||
secs = since_epoch - start_secs;
|
||||
else {
|
||||
sprintf(ebuf, "Warning: %0.3f minutes into the ride, "
|
||||
"time jumps backwards by %0.3f minutes; ignoring it.",
|
||||
secs / 60.0, (secs - since_epoch + start_secs) / 60.0);
|
||||
if (error_cb) error_cb(ebuf, context);
|
||||
ignore = true;
|
||||
}
|
||||
if (time_cb && !ignore) time_cb(&time, since_epoch, context);
|
||||
}
|
||||
else if (PowerTapUtil::is_data(buf, bIsVer81)) {
|
||||
if (wheel_sz_mm == 0) {
|
||||
sprintf(ebuf, "Read data row before wheel size set.");
|
||||
if (error_cb) error_cb(ebuf, context);
|
||||
return;
|
||||
}
|
||||
PowerTapUtil::unpack_data(buf, compat, rec_int_secs, wheel_sz_mm, &secs,
|
||||
&nm, &mph, &watts, &meters, &cad, &hr, bIsVer81);
|
||||
if (compat)
|
||||
miles = round(meters) / 1000.0 * BAD_KM_TO_MI;
|
||||
else
|
||||
miles = meters / 1000.0 * KM_TO_MI;
|
||||
if (data_cb)
|
||||
data_cb(secs, nm, mph, watts, miles, alt, cad,
|
||||
hr, interval, context);
|
||||
}
|
||||
else {
|
||||
sprintf(ebuf, "Unknown record type 0x%x on row %d.", buf[0], row);
|
||||
if (error_cb) error_cb(ebuf, context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (n != -1) {
|
||||
sprintf(ebuf, "Parse error on row %d.", row);
|
||||
if (error_cb) error_cb(ebuf, context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RideFile *RawFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
RideFile *rideFile = new RideFile;
|
||||
rideFile->setDeviceType("PowerTap");
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
}
|
||||
FILE *f = fdopen(file.handle(), "r");
|
||||
assert(f);
|
||||
ReadState state(rideFile, errors);
|
||||
pt_read_raw(f, 0 /* not compat */, &state, config_cb,
|
||||
time_cb, data_cb, error_cb);
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
29
src/RawRideFile.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _RawRideFile_h
|
||||
#define _RawRideFile_h
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
struct RawFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
};
|
||||
|
||||
#endif // _RawRideFile_h
|
||||
|
||||
141
src/RideCalendar.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
#include <QCalendarWidget>
|
||||
#include <QMultiMap>
|
||||
#include <QPainter>
|
||||
#include <QObject>
|
||||
#include <QDate>
|
||||
#include <QAbstractItemView>
|
||||
#include <QSize>
|
||||
#include <QTextCharFormat>
|
||||
#include <QPen>
|
||||
|
||||
#include "RideItem.h"
|
||||
#include "RideCalendar.h"
|
||||
|
||||
RideCalendar::RideCalendar(QWidget *parent)
|
||||
: QCalendarWidget(parent)
|
||||
{
|
||||
};
|
||||
|
||||
void RideCalendar::paintCell(QPainter *painter, const QRect &rect, const QDate &date) const
|
||||
{
|
||||
if (_text.contains(date)) {
|
||||
painter->save();
|
||||
|
||||
/*
|
||||
* Draw a rectangle in the color specified. If this is the
|
||||
* currently selected date, draw a black outline.
|
||||
*/
|
||||
QPen pen(Qt::SolidLine);
|
||||
pen.setCapStyle(Qt::SquareCap);
|
||||
painter->setBrush(_color[date]);
|
||||
if (date == selectedDate()) {
|
||||
pen.setColor(Qt::black);
|
||||
pen.setWidth(1);
|
||||
} else {
|
||||
pen.setColor(_color[date]);
|
||||
}
|
||||
painter->setPen(pen);
|
||||
/*
|
||||
* We have to draw to height-1 and width-1 because Qt draws outlines
|
||||
* outside the box by default.
|
||||
*/
|
||||
painter->drawRect(rect.x(), rect.y(), rect.width() - 1, rect.height() - 1);
|
||||
|
||||
/*
|
||||
* Display the text.
|
||||
*/
|
||||
pen.setColor(Qt::black);
|
||||
painter->setPen(pen);
|
||||
QString text = QString::number(date.day());
|
||||
text = text + "\n" + _text[date];
|
||||
QFont font = painter->font();
|
||||
font.setPointSize(font.pointSize() - 2);
|
||||
painter->setFont(font);
|
||||
painter->drawText(rect, Qt::AlignHCenter | Qt::TextWordWrap, text);
|
||||
|
||||
painter->restore();
|
||||
} else {
|
||||
QCalendarWidget::paintCell(painter, rect, date);
|
||||
}
|
||||
}
|
||||
|
||||
void RideCalendar::setHome(const QDir &homeDir)
|
||||
{
|
||||
home = homeDir;
|
||||
}
|
||||
|
||||
void RideCalendar::addRide(RideItem* ride)
|
||||
{
|
||||
/*
|
||||
* We want to display these things inside the Calendar.
|
||||
* Pick a colour (this should really be configurable)
|
||||
* - red for races
|
||||
* - yellow for sick days
|
||||
* - green for rides
|
||||
*/
|
||||
QDateTime dt = ride->dateTime;
|
||||
QString notesPath = home.absolutePath() + "/" + ride->notesFileName;
|
||||
QFile notesFile(notesPath);
|
||||
QColor color(Qt::green);
|
||||
QString line("Ride");
|
||||
QString code;
|
||||
if (notesFile.exists()) {
|
||||
if (notesFile.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QTextStream in(¬esFile);
|
||||
line = in.readLine();
|
||||
notesFile.close();
|
||||
foreach(code, workoutCodes.keys()) {
|
||||
if (line.contains(code, Qt::CaseInsensitive)) {
|
||||
color = workoutCodes[code];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addEvent(dt.date(), line, color);
|
||||
}
|
||||
|
||||
void RideCalendar::removeRide(RideItem* ride)
|
||||
{
|
||||
removeEvent(ride->dateTime.date());
|
||||
}
|
||||
|
||||
void RideCalendar::addWorkoutCode(QString string, QColor color)
|
||||
{
|
||||
workoutCodes[string] = color;
|
||||
}
|
||||
|
||||
/*
|
||||
* Private:
|
||||
* Add a string, and a color, to a specific date.
|
||||
*/
|
||||
void RideCalendar::addEvent(QDate date, QString string, QColor color)
|
||||
{
|
||||
_text[date] = string;
|
||||
_color[date] = color;
|
||||
update();
|
||||
}
|
||||
|
||||
/*
|
||||
* Private:
|
||||
* Remove the info for a current date.
|
||||
*/
|
||||
void RideCalendar::removeEvent(QDate date)
|
||||
{
|
||||
if (_text.contains(date)) {
|
||||
_text.remove(date);
|
||||
_color.remove(date);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We extend QT's QCalendarWidget's sizeHint() so we claim a little bit of
|
||||
* extra space.
|
||||
*/
|
||||
QSize RideCalendar::sizeHint() const
|
||||
{
|
||||
QSize hint = QCalendarWidget::sizeHint();
|
||||
hint.setHeight(hint.height() * 2);
|
||||
hint.setWidth(hint.width() * 2);
|
||||
return hint;
|
||||
}
|
||||
|
||||
32
src/RideCalendar.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef EVENT_CALENDAR_WIDGET_H
|
||||
#define EVENT_CALENDAR_WIDGET_H
|
||||
|
||||
#include <QCalendarWidget>
|
||||
#include <QMultiMap>
|
||||
#include "RideItem.h"
|
||||
|
||||
class RideCalendar : public QCalendarWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
RideCalendar(QWidget *parent = 0);
|
||||
void removeRide(RideItem*);
|
||||
void addRide(RideItem*);
|
||||
QSize sizeHint() const;
|
||||
void setHome(const QDir&);
|
||||
void addWorkoutCode(QString, QColor);
|
||||
|
||||
protected:
|
||||
void paintCell(QPainter *, const QRect &, const QDate &) const;
|
||||
|
||||
private:
|
||||
void addEvent(QDate, QString, QColor);
|
||||
void removeEvent(QDate);
|
||||
QMap<QDate, QString> _text;
|
||||
QMap<QDate, QColor> _color;
|
||||
QMap<QString, QColor> workoutCodes;
|
||||
QDir home;
|
||||
};
|
||||
|
||||
#endif
|
||||
219
src/RideFile.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideFile.h"
|
||||
#include <QtXml/QtXml>
|
||||
#include <assert.h>
|
||||
#include "Settings.h"
|
||||
|
||||
static void
|
||||
markInterval(QDomDocument &doc, QDomNode &xride, QDomNode &xintervals,
|
||||
double &startSecs, double prevSecs,
|
||||
int &thisInterval, RideFilePoint *sample)
|
||||
{
|
||||
if (xintervals.isNull()) {
|
||||
xintervals = doc.createElement("intervals");
|
||||
xride.appendChild(xintervals);
|
||||
}
|
||||
QDomElement xint = doc.createElement("interval").toElement();
|
||||
xintervals.appendChild(xint);
|
||||
xint.setAttribute("name", thisInterval);
|
||||
xint.setAttribute("from_secs", QString("%1").arg(startSecs, 0, 'f', 2));
|
||||
xint.setAttribute("thru_secs", QString("%1").arg(prevSecs, 0, 'f', 2));
|
||||
startSecs = sample->secs;
|
||||
thisInterval = sample->interval;
|
||||
}
|
||||
|
||||
static void
|
||||
append_text(QDomDocument &doc, QDomNode &parent,
|
||||
const QString &child_name, const QString &child_value)
|
||||
{
|
||||
QDomNode child = parent.appendChild(doc.createElement(child_name));
|
||||
child.appendChild(doc.createTextNode(child_value));
|
||||
}
|
||||
|
||||
bool
|
||||
RideFile::writeAsXml(QFile &file, QString &err) const
|
||||
{
|
||||
(void) err;
|
||||
QDomDocument doc("GoldenCheetah-1.0");
|
||||
QDomNode xride = doc.appendChild(doc.createElement("ride"));
|
||||
QDomNode xheader = xride.appendChild(doc.createElement("header"));
|
||||
append_text(doc, xheader, "start_time", startTime_.toString("yyyy/MM/dd hh:mm:ss"));
|
||||
append_text(doc, xheader, "device_type", deviceType_);
|
||||
append_text(doc, xheader, "rec_int_secs", QString("%1").arg(recIntSecs_, 0, 'f', 3));
|
||||
QDomNode xintervals;
|
||||
bool hasNm = false;
|
||||
double startSecs = 0.0, prevSecs = 0.0;
|
||||
int thisInterval = 0;
|
||||
QListIterator<RideFilePoint*> i(dataPoints_);
|
||||
RideFilePoint *sample = NULL;
|
||||
while (i.hasNext()) {
|
||||
sample = i.next();
|
||||
if (sample->nm > 0.0)
|
||||
hasNm = true;
|
||||
assert(sample->secs >= 0.0);
|
||||
if (sample->interval != thisInterval) {
|
||||
markInterval(doc, xride, xintervals, startSecs,
|
||||
prevSecs, thisInterval, sample);
|
||||
}
|
||||
prevSecs = sample->secs;
|
||||
}
|
||||
if (sample) {
|
||||
markInterval(doc, xride, xintervals, startSecs,
|
||||
prevSecs, thisInterval, sample);
|
||||
}
|
||||
QDomNode xsamples = doc.createElement("samples");
|
||||
xride.appendChild(xsamples);
|
||||
i.toFront();
|
||||
while (i.hasNext()) {
|
||||
RideFilePoint *sample = i.next();
|
||||
QDomElement xsamp = doc.createElement("sample").toElement();
|
||||
xsamples.appendChild(xsamp);
|
||||
xsamp.setAttribute("secs", QString("%1").arg(sample->secs, 0, 'f', 2));
|
||||
xsamp.setAttribute("cad", QString("%1").arg(sample->cad, 0, 'f', 0));
|
||||
xsamp.setAttribute("hr", QString("%1").arg(sample->hr, 0, 'f', 0));
|
||||
xsamp.setAttribute("km", QString("%1").arg(sample->km, 0, 'f', 3));
|
||||
xsamp.setAttribute("kph", QString("%1").arg(sample->kph, 0, 'f', 1));
|
||||
xsamp.setAttribute("alt", QString("%1").arg(sample->alt, 0, 'f', 1));
|
||||
xsamp.setAttribute("watts", sample->watts);
|
||||
if (hasNm) {
|
||||
double nm = (sample->watts > 0.0) ? sample->nm : 0.0;
|
||||
xsamp.setAttribute("nm", QString("%1").arg(nm, 0,'f', 1));
|
||||
}
|
||||
}
|
||||
file.open(QFile::WriteOnly);
|
||||
QTextStream ts(&file);
|
||||
doc.save(ts, 4);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RideFile::writeAsCsv(QFile &file, bool bIsMetric) const
|
||||
{
|
||||
|
||||
// Use the column headers that make WKO+ happy.
|
||||
double convertUnit;
|
||||
QTextStream out(&file);
|
||||
if (!bIsMetric)
|
||||
{
|
||||
out << "Minutes,Torq (N-m),MPH,Watts,Miles,Cadence,Hrate,ID,Altitude (feet)\n";
|
||||
const double MILES_PER_KM = 0.62137119;
|
||||
convertUnit = MILES_PER_KM;
|
||||
}
|
||||
else {
|
||||
out << "Minutes,Torq (N-m),Km/h,Watts,Km,Cadence,Hrate,ID,Altitude (m)\n";
|
||||
// TODO: use KM_TO_MI from lib/pt.c instead?
|
||||
convertUnit = 1.0;
|
||||
}
|
||||
|
||||
QListIterator<RideFilePoint*> i(dataPoints());
|
||||
while (i.hasNext()) {
|
||||
RideFilePoint *point = i.next();
|
||||
if (point->secs == 0.0)
|
||||
continue;
|
||||
out << point->secs/60.0;
|
||||
out << ",";
|
||||
out << ((point->nm >= 0) ? point->nm : 0.0);
|
||||
out << ",";
|
||||
out << ((point->kph >= 0) ? (point->kph * convertUnit) : 0.0);
|
||||
out << ",";
|
||||
out << ((point->watts >= 0) ? point->watts : 0.0);
|
||||
out << ",";
|
||||
out << point->km * convertUnit;
|
||||
out << ",";
|
||||
out << point->cad;
|
||||
out << ",";
|
||||
out << point->hr;
|
||||
out << ",";
|
||||
out << point->interval;
|
||||
out << ",";
|
||||
out << point->alt;
|
||||
if (point->bs > 0.0) {
|
||||
out << ",";
|
||||
out << point->bs;
|
||||
}
|
||||
out << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
RideFileFactory *RideFileFactory::instance_;
|
||||
|
||||
RideFileFactory &RideFileFactory::instance()
|
||||
{
|
||||
if (!instance_)
|
||||
instance_ = new RideFileFactory();
|
||||
return *instance_;
|
||||
}
|
||||
|
||||
int RideFileFactory::registerReader(const QString &suffix,
|
||||
RideFileReader *reader)
|
||||
{
|
||||
assert(!readFuncs_.contains(suffix));
|
||||
readFuncs_.insert(suffix, reader);
|
||||
return 1;
|
||||
}
|
||||
|
||||
RideFile *RideFileFactory::openRideFile(QFile &file,
|
||||
QStringList &errors) const
|
||||
{
|
||||
QString suffix = file.fileName();
|
||||
int dot = suffix.lastIndexOf(".");
|
||||
assert(dot >= 0);
|
||||
suffix.remove(0, dot + 1);
|
||||
RideFileReader *reader = readFuncs_.value(suffix.toLower());
|
||||
assert(reader);
|
||||
return reader->openRideFile(file, errors);
|
||||
}
|
||||
|
||||
QStringList RideFileFactory::listRideFiles(const QDir &dir) const
|
||||
{
|
||||
QStringList filters;
|
||||
QMapIterator<QString,RideFileReader*> i(readFuncs_);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
filters << ("*." + i.key());
|
||||
}
|
||||
// This will read the user preferences and change the file list order as necessary:
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();;
|
||||
|
||||
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked);
|
||||
if(isAscending.toInt()>0){
|
||||
return dir.entryList(filters, QDir::Files, QDir::Name);
|
||||
}
|
||||
return dir.entryList(filters, QDir::Files, QDir::Name|QDir::Reversed);
|
||||
}
|
||||
|
||||
void RideFile::appendPoint(double secs, double cad, double hr, double km,
|
||||
double kph, double nm, double watts, double alt,
|
||||
int interval, double bs)
|
||||
{
|
||||
dataPoints_.append(new RideFilePoint(secs, cad, hr, km, kph,
|
||||
nm, watts, alt, interval,bs));
|
||||
dataPresent.secs |= (secs != 0);
|
||||
dataPresent.cad |= (cad != 0);
|
||||
dataPresent.hr |= (hr != 0);
|
||||
dataPresent.km |= (km != 0);
|
||||
dataPresent.kph |= (kph != 0);
|
||||
dataPresent.nm |= (nm != 0);
|
||||
dataPresent.watts |= (watts != 0);
|
||||
dataPresent.alt |= (alt != 0);
|
||||
dataPresent.interval |= (interval != 0);
|
||||
}
|
||||
131
src/RideFile.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2007 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _RideFile_h
|
||||
#define _RideFile_h
|
||||
|
||||
#include <QDate>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
|
||||
// This file defines four classes:
|
||||
//
|
||||
// RideFile, as the name suggests, represents the data stored in a ride file,
|
||||
// regardless of what type of file it is (.raw, .srm, .csv).
|
||||
//
|
||||
// RideFilePoint represents the data for a single sample in a RideFile.
|
||||
//
|
||||
// RideFileReader is an abstract base class for function-objects that take a
|
||||
// filename and return a RideFile object representing the ride stored in the
|
||||
// corresponding file.
|
||||
//
|
||||
// RideFileFactory is a singleton that maintains a mapping from ride file
|
||||
// suffixes to the RideFileReader objects capable of converting those files
|
||||
// into RideFile objects.
|
||||
|
||||
struct RideFilePoint
|
||||
{
|
||||
double secs, cad, hr, km, kph, nm, watts, alt;
|
||||
int interval;
|
||||
double bs; // to init in order
|
||||
RideFilePoint() : secs(0.0), cad(0.0), hr(0.0), km(0.0), kph(0.0),
|
||||
nm(0.0), watts(0.0), alt(0.0), interval(0), bs(0.0) {}
|
||||
RideFilePoint(double secs, double cad, double hr, double km, double kph,
|
||||
double nm, double watts, double alt, int interval, double bs) :
|
||||
secs(secs), cad(cad), hr(hr), km(km), kph(kph), nm(nm),
|
||||
watts(watts), alt(alt), interval(interval), bs(bs) {}
|
||||
};
|
||||
|
||||
struct RideFileDataPresent
|
||||
{
|
||||
bool secs, cad, hr, km, kph, nm, watts, alt, interval;
|
||||
// whether non-zero data of each field is present
|
||||
RideFileDataPresent():
|
||||
secs(false), cad(false), hr(false), km(false),
|
||||
kph(false), nm(false), watts(false), alt(false), interval(false) {}
|
||||
};
|
||||
|
||||
class RideFile
|
||||
{
|
||||
private:
|
||||
|
||||
QDateTime startTime_; // time of day that the ride started
|
||||
double recIntSecs_; // recording interval in seconds
|
||||
QList<RideFilePoint*> dataPoints_;
|
||||
RideFileDataPresent dataPresent;
|
||||
QString deviceType_;
|
||||
|
||||
public:
|
||||
|
||||
RideFile() : recIntSecs_(0.0), deviceType_("unknown") {}
|
||||
RideFile(const QDateTime &startTime, double recIntSecs) :
|
||||
startTime_(startTime), recIntSecs_(recIntSecs),
|
||||
deviceType_("unknown") {}
|
||||
|
||||
virtual ~RideFile() {
|
||||
QListIterator<RideFilePoint*> i(dataPoints_);
|
||||
while (i.hasNext())
|
||||
delete i.next();
|
||||
}
|
||||
|
||||
const QDateTime &startTime() const { return startTime_; }
|
||||
double recIntSecs() const { return recIntSecs_; }
|
||||
const QList<RideFilePoint*> dataPoints() const { return dataPoints_; }
|
||||
inline RideFileDataPresent *areDataPresent() { return &dataPresent; }
|
||||
const QString &deviceType() const { return deviceType_; }
|
||||
|
||||
void setStartTime(const QDateTime &value) { startTime_ = value; }
|
||||
void setRecIntSecs(double value) { recIntSecs_ = value; }
|
||||
void setDeviceType(const QString &value) { deviceType_ = value; }
|
||||
|
||||
void appendPoint(double secs, double cad, double hr, double km,
|
||||
double kph, double nm, double watts, double alt, int interval, double bs=0.0);
|
||||
|
||||
bool writeAsXml(QFile &file, QString &err) const;
|
||||
void writeAsCsv(QFile &file, bool bIsMetric) const;
|
||||
|
||||
void resetDataPresent();
|
||||
};
|
||||
|
||||
struct RideFileReader {
|
||||
virtual ~RideFileReader() {}
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const = 0;
|
||||
};
|
||||
|
||||
class RideFileFactory {
|
||||
|
||||
private:
|
||||
|
||||
static RideFileFactory *instance_;
|
||||
QMap<QString,RideFileReader*> readFuncs_;
|
||||
|
||||
RideFileFactory() {}
|
||||
|
||||
public:
|
||||
|
||||
static RideFileFactory &instance();
|
||||
|
||||
int registerReader(const QString &suffix, RideFileReader *reader);
|
||||
RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
QStringList listRideFiles(const QDir &dir) const;
|
||||
};
|
||||
|
||||
#endif // _RideFile_h
|
||||
|
||||
446
src/RideItem.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideItem.h"
|
||||
#include "RideMetric.h"
|
||||
#include "RideFile.h"
|
||||
#include "Settings.h"
|
||||
#include "TimeUtils.h"
|
||||
#include "Zones.h"
|
||||
#include <iostream> // delete me
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <QtXml/QtXml>
|
||||
|
||||
#define MILES_PER_KM 0.62137119
|
||||
|
||||
RideItem::RideItem(int type,
|
||||
QString path, QString fileName, const QDateTime &dateTime,
|
||||
Zones **zones, QString notesFileName) :
|
||||
QTreeWidgetItem(type), path(path), fileName(fileName),
|
||||
dateTime(dateTime), zones(zones), notesFileName(notesFileName)
|
||||
{
|
||||
setText(0, dateTime.toString("ddd"));
|
||||
setText(1, dateTime.toString("MMM d, yyyy"));
|
||||
setText(2, dateTime.toString("h:mm AP"));
|
||||
setTextAlignment(1, Qt::AlignRight);
|
||||
setTextAlignment(2, Qt::AlignRight);
|
||||
|
||||
time_in_zone = NULL;
|
||||
}
|
||||
|
||||
RideItem::~RideItem()
|
||||
{
|
||||
MetricIter i(metrics);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
delete i.value();
|
||||
}
|
||||
}
|
||||
|
||||
static void summarize(QString &intervals,
|
||||
unsigned last_interval,
|
||||
double km_start, double km_end,
|
||||
double &int_watts_sum,
|
||||
double &int_hr_sum,
|
||||
double &int_cad_sum,
|
||||
double &int_kph_sum,
|
||||
double &int_secs_hr,
|
||||
double &int_max_power,
|
||||
double int_dur)
|
||||
{
|
||||
double dur = int_dur;
|
||||
double mile_len = (km_end - km_start) * MILES_PER_KM;
|
||||
double minutes = (int) (dur/60.0);
|
||||
double seconds = dur - (60 * minutes);
|
||||
double watts_avg = int_watts_sum / dur;
|
||||
double hr_avg = int_hr_sum / int_secs_hr;
|
||||
double cad_avg = int_cad_sum / dur;
|
||||
double mph_avg = int_kph_sum * MILES_PER_KM / dur;
|
||||
double energy = int_watts_sum / 1000.0; // watts_avg / 1000.0 * dur;
|
||||
|
||||
intervals += "<tr><td align=\"center\">%1</td>";
|
||||
intervals += "<td align=\"center\">%2:%3</td>";
|
||||
intervals += "<td align=\"center\">%4</td>";
|
||||
intervals += "<td align=\"center\">%5</td>";
|
||||
intervals += "<td align=\"center\">%6</td>";
|
||||
intervals += "<td align=\"center\">%7</td>";
|
||||
intervals += "<td align=\"center\">%8</td>";
|
||||
intervals += "<td align=\"center\">%9</td>";
|
||||
intervals += "<td align=\"center\">%10</td>";
|
||||
intervals = intervals.arg(last_interval);
|
||||
intervals = intervals.arg(minutes, 0, 'f', 0);
|
||||
intervals = intervals.arg(seconds, 2, 'f', 0, QLatin1Char('0'));
|
||||
intervals = intervals.arg(mile_len, 0, 'f', 1);
|
||||
intervals = intervals.arg(energy, 0, 'f', 0);
|
||||
intervals = intervals.arg(int_max_power, 0, 'f', 0);
|
||||
intervals = intervals.arg(watts_avg, 0, 'f', 0);
|
||||
intervals = intervals.arg(hr_avg, 0, 'f', 0);
|
||||
intervals = intervals.arg(cad_avg, 0, 'f', 0);
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
|
||||
QVariant unit = settings->value(GC_UNIT);
|
||||
if(unit.toString() == "Metric")
|
||||
intervals = intervals.arg(mph_avg * 1.60934, 0, 'f', 1);
|
||||
else
|
||||
intervals = intervals.arg(mph_avg, 0, 'f', 1);
|
||||
|
||||
int_watts_sum = 0.0;
|
||||
int_hr_sum = 0.0;
|
||||
int_cad_sum = 0.0;
|
||||
int_kph_sum = 0.0;
|
||||
int_max_power = 0.0;
|
||||
}
|
||||
|
||||
int RideItem::zoneRange()
|
||||
{
|
||||
return (
|
||||
(zones && *zones) ?
|
||||
(*zones)->whichRange(dateTime.date()) :
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
int RideItem::numZones()
|
||||
{
|
||||
if (zones && *zones) {
|
||||
int zone_range = zoneRange();
|
||||
return ((zone_range >= 0) ?
|
||||
(*zones)->numZones(zone_range) :
|
||||
0
|
||||
);
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
double RideItem::timeInZone(int zone)
|
||||
{
|
||||
htmlSummary();
|
||||
if (!ride)
|
||||
return 0.0;
|
||||
assert(zone < numZones());
|
||||
return time_in_zone[zone];
|
||||
}
|
||||
|
||||
static const char *metricsXml =
|
||||
"<metrics>\n"
|
||||
" <metric_group name=\"Totals\">\n"
|
||||
" <metric name=\"workout_time\" display_name=\"Workout time\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"time_riding\" display_name=\"Time riding\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"total_distance\" display_name=\"Distance\"\n"
|
||||
" precision=\"1\"/>\n"
|
||||
" <metric name=\"total_work\" display_name=\"Work\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"elevation_gain\" display_name=\"Elevation Gain\"\n"
|
||||
" precision=\"1\"/>\n"
|
||||
" </metric_group>\n"
|
||||
" <metric_group name=\"Averages\">\n"
|
||||
" <metric name=\"average_speed\" display_name=\"Speed\"\n"
|
||||
" precision=\"1\"/>\n"
|
||||
" <metric name=\"average_power\" display_name=\"Power\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"average_hr\" display_name=\"Heart rate\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"average_cad\" display_name=\"Cadence\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" </metric_group>\n"
|
||||
" <metric_group name=\"BikeScore™\" note=\"BikeScore is a trademark "
|
||||
" of Dr. Philip Friere Skiba, PhysFarm Training Systems LLC\">\n"
|
||||
" <metric name=\"skiba_xpower\" display_name=\"xPower\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" <metric name=\"skiba_relative_intensity\"\n"
|
||||
" display_name=\"Relative Intensity\" precision=\"3\"/>\n"
|
||||
" <metric name=\"skiba_bike_score\" display_name=\"BikeScore\"\n"
|
||||
" precision=\"0\"/>\n"
|
||||
" </metric_group>\n"
|
||||
"</metrics>\n";
|
||||
|
||||
QString
|
||||
RideItem::htmlSummary()
|
||||
{
|
||||
if (summary.isEmpty() ||
|
||||
(zones && *zones && (summaryGenerationTime < (*zones)->modificationTime))) {
|
||||
// set defaults for zone range and number of zones
|
||||
int zone_range = -1;
|
||||
int num_zones = 0;
|
||||
|
||||
summaryGenerationTime = QDateTime::currentDateTime();
|
||||
|
||||
QFile file(path + "/" + fileName);
|
||||
QStringList errors;
|
||||
ride = RideFileFactory::instance().openRideFile(file, errors);
|
||||
if (!ride) {
|
||||
summary = "<p>Couldn't read file \"" + file.fileName() + "\":";
|
||||
QListIterator<QString> i(errors);
|
||||
while (i.hasNext())
|
||||
summary += "<br>" + i.next();
|
||||
return summary;
|
||||
}
|
||||
summary = ("<p><center><h2>"
|
||||
+ dateTime.toString("dddd MMMM d, yyyy, h:mm AP")
|
||||
+ "</h2><h3>Device Type: " + ride->deviceType() + "</h3>");
|
||||
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
QVariant unit = settings->value(GC_UNIT);
|
||||
|
||||
if (zones &&
|
||||
*zones &&
|
||||
((zone_range = (*zones)->whichRange(dateTime.date())) >= 0) &&
|
||||
((num_zones = (*zones)->numZones(zone_range)) > 0)
|
||||
)
|
||||
{
|
||||
time_in_zone = new double[num_zones];
|
||||
for (int i = 0; i < num_zones; ++i)
|
||||
time_in_zone[i] = 0.0;
|
||||
}
|
||||
|
||||
const RideMetricFactory &factory = RideMetricFactory::instance();
|
||||
QSet<QString> todo;
|
||||
|
||||
// hack djconnel: do the metrics TWICE, to catch dependencies
|
||||
// on displayed variables. Presently if a variable depends on zones,
|
||||
// for example, and zones change, the value may be considered still
|
||||
// value even though it will change. This is presently happening
|
||||
// where bikescore depends on relative intensity.
|
||||
// note metrics are only calculated if zones are defined
|
||||
for (int metriciteration = 0; metriciteration < 2; metriciteration ++) {
|
||||
for (int i = 0; i < factory.metricCount(); ++i) {
|
||||
todo.insert(factory.metricName(i));
|
||||
|
||||
while (!todo.empty()) {
|
||||
QMutableSetIterator<QString> i(todo);
|
||||
later:
|
||||
while (i.hasNext()) {
|
||||
const QString &name = i.next();
|
||||
const QVector<QString> &deps = factory.dependencies(name);
|
||||
for (int j = 0; j < deps.size(); ++j)
|
||||
if (!metrics.contains(deps[j]))
|
||||
goto later;
|
||||
RideMetric *metric = factory.newMetric(name);
|
||||
metric->compute(ride, *zones, zone_range, metrics);
|
||||
metrics.insert(name, metric);
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double secs_watts = 0.0;
|
||||
|
||||
QString intervals = "";
|
||||
int interval_count = 0;
|
||||
int last_interval = INT_MAX;
|
||||
double int_watts_sum = 0.0;
|
||||
double int_hr_sum = 0.0;
|
||||
double int_cad_sum = 0.0;
|
||||
double int_kph_sum = 0.0;
|
||||
double int_secs_hr = 0.0;
|
||||
double int_max_power = 0.0;
|
||||
|
||||
double time_start, time_end, km_start, km_end, int_dur;
|
||||
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
while (i.hasNext()) {
|
||||
RideFilePoint *point = i.next();
|
||||
|
||||
double secs_delta = ride->recIntSecs();
|
||||
|
||||
if (point->interval != last_interval) {
|
||||
|
||||
if (last_interval != INT_MAX) {
|
||||
summarize(intervals, last_interval,
|
||||
km_start, km_end, int_watts_sum,
|
||||
int_hr_sum, int_cad_sum, int_kph_sum,
|
||||
int_secs_hr, int_max_power, int_dur);
|
||||
}
|
||||
interval_count++;
|
||||
last_interval = point->interval;
|
||||
time_start = point->secs;
|
||||
km_start = point->km;
|
||||
int_secs_hr = secs_delta;
|
||||
int_dur = 0.0;
|
||||
}
|
||||
|
||||
if ((point->kph > 0.0) || (point->cad > 0.0)) {
|
||||
int_dur += secs_delta;
|
||||
}
|
||||
if (point->watts >= 0.0) {
|
||||
secs_watts += secs_delta;
|
||||
int_watts_sum += point->watts * secs_delta;
|
||||
if (point->watts > int_max_power)
|
||||
int_max_power = point->watts;
|
||||
if (num_zones > 0) {
|
||||
int zone = (*zones)->whichZone(zone_range, point->watts);
|
||||
if (zone >= 0)
|
||||
time_in_zone[zone] += secs_delta;
|
||||
}
|
||||
}
|
||||
if (point->hr > 0) {
|
||||
int_hr_sum += point->hr * secs_delta;
|
||||
int_secs_hr += secs_delta;
|
||||
}
|
||||
if (point->cad > 0)
|
||||
int_cad_sum += point->cad * secs_delta;
|
||||
if (point->kph >= 0)
|
||||
int_kph_sum += point->kph * secs_delta;
|
||||
|
||||
km_end = point->km;
|
||||
time_end = point->secs + secs_delta;
|
||||
}
|
||||
summarize(intervals, last_interval,
|
||||
km_start, km_end, int_watts_sum,
|
||||
int_hr_sum, int_cad_sum, int_kph_sum,
|
||||
int_secs_hr, int_max_power, int_dur);
|
||||
|
||||
summary += "<p>";
|
||||
|
||||
bool metricUnits = (unit.toString() == "Metric");
|
||||
|
||||
QDomDocument doc;
|
||||
{
|
||||
QString err;
|
||||
int errLine, errCol;
|
||||
if (!doc.setContent(QString(metricsXml), &err, &errLine, &errCol)){
|
||||
fprintf(stderr, "error: %s, line %d, col %d\n",
|
||||
err.toAscii().constData(), errLine, errCol);
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
QString noteString = "";
|
||||
QString stars;
|
||||
QDomNodeList groups = doc.elementsByTagName("metric_group");
|
||||
for (int groupNum = 0; groupNum < groups.size(); ++groupNum) {
|
||||
QDomElement group = groups.at(groupNum).toElement();
|
||||
assert(!group.isNull());
|
||||
QString groupName = group.attribute("name");
|
||||
QString groupNote = group.attribute("note");
|
||||
assert(groupName.length() > 0);
|
||||
if (groupNum % 2 == 0)
|
||||
summary += "<table border=0 cellspacing=10><tr>";
|
||||
summary += "<td align=\"center\" width=\"45%\"><table>"
|
||||
"<tr><td align=\"center\" colspan=2><h2>%1</h2></td></tr>";
|
||||
if (groupNote.length() > 0) {
|
||||
stars += "*";
|
||||
summary = summary.arg(groupName + stars);
|
||||
noteString += "<br>" + stars + " " + groupNote;
|
||||
}
|
||||
else {
|
||||
summary = summary.arg(groupName);
|
||||
}
|
||||
QDomNodeList metricsList = group.childNodes();
|
||||
for (int i = 0; i < metricsList.size(); ++i) {
|
||||
QDomElement metric = metricsList.at(i).toElement();
|
||||
QString name = metric.attribute("name");
|
||||
QString displayName = metric.attribute("display_name");
|
||||
int precision = metric.attribute("precision", "0").toInt();
|
||||
assert(name.length() > 0);
|
||||
assert(displayName.length() > 0);
|
||||
const RideMetric *m = metrics.value(name);
|
||||
assert(m);
|
||||
if (m->units(metricUnits) == "seconds") {
|
||||
QString s("<tr><td>%1:</td><td "
|
||||
"align=\"right\">%2</td></tr>");
|
||||
s = s.arg(displayName);
|
||||
s = s.arg(time_to_string(m->value(metricUnits)));
|
||||
summary += s;
|
||||
}
|
||||
else {
|
||||
QString s = "<tr><td>" + displayName;
|
||||
if (m->units(metricUnits) != "")
|
||||
s += " (" + m->units(metricUnits) + ")";
|
||||
s += ":</td><td align=\"right\">%1</td></tr>";
|
||||
if (precision == 0)
|
||||
s = s.arg((unsigned) round(m->value(metricUnits)));
|
||||
else
|
||||
s = s.arg(m->value(metricUnits), 0, 'f', precision);
|
||||
summary += s;
|
||||
}
|
||||
}
|
||||
summary += "</table></td>";
|
||||
if ((groupNum % 2 == 1) || (groupNum == groups.size() - 1))
|
||||
summary += "</tr></table>";
|
||||
}
|
||||
|
||||
if (num_zones > 0) {
|
||||
summary += "<h2>Power Zones</h2>";
|
||||
summary += (*zones)->summarize(zone_range, time_in_zone, num_zones);
|
||||
}
|
||||
|
||||
// TODO: Ergomo uses non-consecutive interval numbers.
|
||||
// Seems to use 0 when not in an interval
|
||||
// and an integer < 30 when in an interval.
|
||||
// We'll need to create a counter for the intervals
|
||||
// rather than relying on the final data point's interval number.
|
||||
if (interval_count > 1) {
|
||||
summary += "<p><h2>Intervals</h2>\n<p>\n";
|
||||
summary += "<table align=\"center\" width=\"90%\" ";
|
||||
summary += "cellspacing=0 border=0><tr>";
|
||||
summary += "<td align=\"center\">Interval</td>";
|
||||
summary += "<td align=\"center\"></td>";
|
||||
summary += "<td align=\"center\">Distance</td>";
|
||||
summary += "<td align=\"center\">Work</td>";
|
||||
summary += "<td align=\"center\">Max Power</td>";
|
||||
summary += "<td align=\"center\">Avg Power</td>";
|
||||
summary += "<td align=\"center\">Avg HR</td>";
|
||||
summary += "<td align=\"center\">Avg Cadence</td>";
|
||||
summary += "<td align=\"center\">Avg Speed</td>";
|
||||
summary += "</tr><tr>";
|
||||
summary += "<td align=\"center\">Number</td>";
|
||||
summary += "<td align=\"center\">Duration</td>";
|
||||
if(unit.toString() == "Metric")
|
||||
summary += "<td align=\"center\">(km)</td>";
|
||||
else
|
||||
summary += "<td align=\"center\">(miles)</td>";
|
||||
summary += "<td align=\"center\">(kJ)</td>";
|
||||
summary += "<td align=\"center\">(watts)</td>";
|
||||
summary += "<td align=\"center\">(watts)</td>";
|
||||
summary += "<td align=\"center\">(bpm)</td>";
|
||||
summary += "<td align=\"center\">(rpm)</td>";
|
||||
if(unit.toString() == "Metric")
|
||||
summary += "<td align=\"center\">(km/h)</td>";
|
||||
else
|
||||
summary += "<td align=\"center\">(mph)</td>";
|
||||
summary += "</tr>";
|
||||
summary += intervals;
|
||||
summary += "</table>";
|
||||
}
|
||||
|
||||
if (!errors.empty()) {
|
||||
summary += "<p><h2>Errors reading file:</h2><ul>";
|
||||
QStringListIterator i(errors);
|
||||
while(i.hasNext())
|
||||
summary += " <li>" + i.next();
|
||||
summary += "</ul>";
|
||||
}
|
||||
if (noteString.length() > 0) {
|
||||
// The extra </center><center> works around a bug in QT 4.3.1,
|
||||
// which will otherwise put the noteString above the <hr>.
|
||||
summary += "<br><hr width=\"80%\"></center><center>" + noteString;
|
||||
}
|
||||
summary += "</center>";
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
64
src/RideItem.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_RideItem_h
|
||||
#define _GC_RideItem_h 1
|
||||
|
||||
#include <QtGui>
|
||||
|
||||
class RideFile;
|
||||
class Zones;
|
||||
class RideMetric;
|
||||
|
||||
class RideItem : public QTreeWidgetItem {
|
||||
|
||||
protected:
|
||||
|
||||
double *time_in_zone;
|
||||
|
||||
public:
|
||||
|
||||
QString path;
|
||||
QString fileName;
|
||||
QDateTime dateTime;
|
||||
QString summary;
|
||||
QDateTime summaryGenerationTime;
|
||||
RideFile *ride;
|
||||
Zones **zones;
|
||||
QString notesFileName;
|
||||
|
||||
typedef QHash<QString,RideMetric*> MetricMap;
|
||||
typedef QHashIterator<QString,RideMetric*> MetricIter;
|
||||
|
||||
MetricMap metrics;
|
||||
|
||||
RideItem(int type, QString path,
|
||||
QString fileName, const QDateTime &dateTime,
|
||||
Zones **zones, QString notesFileName);
|
||||
|
||||
~RideItem();
|
||||
|
||||
QString htmlSummary();
|
||||
|
||||
int zoneRange();
|
||||
int numZones();
|
||||
double timeInZone(int zone);
|
||||
};
|
||||
|
||||
#endif // _GC_RideItem_h
|
||||
|
||||
23
src/RideMetric.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideMetric.h"
|
||||
|
||||
RideMetricFactory *RideMetricFactory::_instance;
|
||||
QVector<QString> RideMetricFactory::noDeps;
|
||||
|
||||
138
src/RideMetric.h
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_RideMetric_h
|
||||
#define _GC_RideMetric_h 1
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <assert.h>
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
class Zones;
|
||||
|
||||
struct RideMetric {
|
||||
virtual ~RideMetric() {}
|
||||
virtual QString name() const = 0;
|
||||
virtual QString units(bool metric) const = 0;
|
||||
virtual double value(bool metric) const = 0;
|
||||
virtual void compute(const RideFile *ride,
|
||||
const Zones *zones,
|
||||
int zoneRange,
|
||||
const QHash<QString,RideMetric*> &deps) = 0;
|
||||
virtual bool canAggregate() const { return false; }
|
||||
virtual void aggregateWith(RideMetric *other) {
|
||||
(void) other;
|
||||
assert(false);
|
||||
}
|
||||
virtual RideMetric *clone() const = 0;
|
||||
};
|
||||
|
||||
struct PointwiseRideMetric : public RideMetric {
|
||||
void compute(const RideFile *ride, const Zones *zones, int zoneRange,
|
||||
const QHash<QString,RideMetric*> &) {
|
||||
QListIterator<RideFilePoint*> i(ride->dataPoints());
|
||||
while (i.hasNext()) {
|
||||
const RideFilePoint *point = i.next();
|
||||
perPoint(point, ride->recIntSecs(), ride, zones, zoneRange);
|
||||
}
|
||||
}
|
||||
virtual void perPoint(const RideFilePoint *point, double secsDelta,
|
||||
const RideFile *ride, const Zones *zones,
|
||||
int zoneRange) = 0;
|
||||
};
|
||||
|
||||
class AvgRideMetric : public PointwiseRideMetric {
|
||||
|
||||
protected:
|
||||
|
||||
int count;
|
||||
double total;
|
||||
|
||||
public:
|
||||
|
||||
AvgRideMetric() : count(0), total(0.0) {}
|
||||
double value(bool) const {
|
||||
if (count == 0) return 0.0;
|
||||
return total / count;
|
||||
}
|
||||
void aggregateWith(RideMetric *other) {
|
||||
assert(name() == other->name());
|
||||
AvgRideMetric *as = dynamic_cast<AvgRideMetric*>(other);
|
||||
count += as->count;
|
||||
total += as->total;
|
||||
}
|
||||
};
|
||||
|
||||
class RideMetricFactory {
|
||||
|
||||
static RideMetricFactory *_instance;
|
||||
static QVector<QString> noDeps;
|
||||
|
||||
QVector<QString> metricNames;
|
||||
QHash<QString,RideMetric*> metrics;
|
||||
QHash<QString,QVector<QString>*> dependencyMap;
|
||||
|
||||
RideMetricFactory() {}
|
||||
RideMetricFactory(const RideMetricFactory &other);
|
||||
RideMetricFactory &operator=(const RideMetricFactory &other);
|
||||
|
||||
public:
|
||||
|
||||
static RideMetricFactory &instance() {
|
||||
if (!_instance)
|
||||
_instance = new RideMetricFactory();
|
||||
return *_instance;
|
||||
}
|
||||
|
||||
int metricCount() const { return metricNames.size(); }
|
||||
|
||||
const QString &metricName(int i) const { return metricNames[i]; }
|
||||
|
||||
RideMetric *newMetric(const QString &name) const {
|
||||
assert(metrics.contains(name));
|
||||
return metrics.value(name)->clone();
|
||||
}
|
||||
|
||||
bool addMetric(const RideMetric &metric,
|
||||
const QVector<QString> *deps = NULL) {
|
||||
assert(!metrics.contains(metric.name()));
|
||||
metrics.insert(metric.name(), metric.clone());
|
||||
metricNames.append(metric.name());
|
||||
if (deps) {
|
||||
QVector<QString> *copy = new QVector<QString>;
|
||||
for (int i = 0; i < deps->size(); ++i) {
|
||||
assert(metrics.contains((*deps)[i]));
|
||||
copy->append((*deps)[i]);
|
||||
}
|
||||
dependencyMap.insert(metric.name(), copy);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const QVector<QString> &dependencies(const QString &name) const {
|
||||
assert(metrics.contains(name));
|
||||
QVector<QString> *result = dependencyMap.value(name);
|
||||
return result ? *result : noDeps;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // _GC_RideMetric_h
|
||||
|
||||