Files
GoldenCheetah/src/DialWindow.cpp
Mark Liversedge 1e8b6edb62 Show kJoules, TSS/BikeScore et al on Train View
The refactoring of the realtime display last year
removed the display of metrics such as BikeScore and
kJoules.

This patch adds more metrics that can be displayed;
* Averages for; power, hr, cadence, speed
* KJoules of work
* Coggan Metrics; NP, TSS, IF, VI
* Skiba Metrics; xPower, BikeScore, RI, Skiba VI

Also included is an updated default layout to
include some of these metrics.

Fixes #231
2011-11-04 16:28:36 +00:00

410 lines
12 KiB
C++

/*
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "DialWindow.h"
DialWindow::DialWindow(MainWindow *mainWindow) :
GcWindow(mainWindow), mainWindow(mainWindow)
{
rolling.resize(150); // enough for 30 seconds at 5hz
setContentsMargins(0,0,0,0);
setInstanceName("Dial");
QWidget *c = new QWidget;
QVBoxLayout *cl = new QVBoxLayout(c);
setControls(c);
// setup the controls
QFormLayout *controlsLayout = new QFormLayout();
controlsLayout->setSpacing(0);
controlsLayout->setContentsMargins(5,5,5,5);
cl->addLayout(controlsLayout);
// data series selection
QLabel *seriesLabel = new QLabel(tr("Data Series"), this);
seriesLabel->setAutoFillBackground(true);
seriesSelector = new QComboBox(this);
foreach (RealtimeData::DataSeries x, RealtimeData::listDataSeries()) {
seriesSelector->addItem(RealtimeData::seriesName(x), static_cast<int>(x));
}
controlsLayout->addRow(seriesLabel, seriesSelector);
// display label...
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setSpacing(0);
layout->setContentsMargins(3,3,3,3);
valueLabel = new QLabel(this);
valueLabel->setAlignment(Qt::AlignCenter | Qt::AlignVCenter);
layout->addWidget(valueLabel);
// get updates..
connect(mainWindow, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData)));
connect(seriesSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(seriesChanged()));
connect(mainWindow, SIGNAL(configChanged()), this, SLOT(seriesChanged()));
connect(mainWindow, SIGNAL(stop()), this, SLOT(stop()));
connect(mainWindow, SIGNAL(start()), this, SLOT(start()));
// setup colors
seriesChanged();
// setup fontsize etc
resizeEvent(NULL);
// set to zero
resetValues();
}
void
DialWindow::lap(int lapnumber)
{
lapNumber = lapnumber;
avgLap = 0;
}
void
DialWindow::start()
{
resetValues();
}
void
DialWindow::stop()
{
resetValues();
}
void
DialWindow::pause()
{
}
void
DialWindow::telemetryUpdate(const RealtimeData &rtData)
{
// we got some!
RealtimeData::DataSeries series = static_cast<RealtimeData::DataSeries>
(seriesSelector->itemData(seriesSelector->currentIndex()).toInt());
double value = rtData.value(series);
switch (series) {
case RealtimeData::Time:
case RealtimeData::LapTime:
{
long msecs = value;
valueLabel->setText(QString("%1:%2:%3.%4").arg(msecs/3600000)
.arg((msecs%3600000)/60000,2,10,QLatin1Char('0'))
.arg((msecs%60000)/1000,2,10,QLatin1Char('0'))
.arg((msecs%1000)/100));
}
break;
case RealtimeData::Speed:
if (!mainWindow->useMetricUnits) value *= MILES_PER_KM;
valueLabel->setText(QString("%1").arg(value, 0, 'f', 1));
break;
case RealtimeData::Distance:
if (!mainWindow->useMetricUnits) value *= MILES_PER_KM;
valueLabel->setText(QString("%1").arg(value, 0, 'f', 3));
break;
case RealtimeData::AvgWatts:
sum += rtData.value(RealtimeData::Watts);
count++;
value = sum / count;
valueLabel->setText(QString("%1").arg(round(value)));
break;
case RealtimeData::AvgSpeed:
sum += rtData.value(RealtimeData::Speed);
count++;
value = sum / count;
if (!mainWindow->useMetricUnits) value *= MILES_PER_KM;
valueLabel->setText(QString("%1").arg(value, 0, 'f', 1));
break;
case RealtimeData::AvgCadence:
sum += rtData.value(RealtimeData::Cadence);
count++;
value = sum / count;
valueLabel->setText(QString("%1").arg(round(value)));
break;
case RealtimeData::AvgHeartRate:
sum += rtData.value(RealtimeData::HeartRate);
count++;
value = sum / count;
valueLabel->setText(QString("%1").arg(round(value)));
break;
// ENERGY
case RealtimeData::Joules:
sum += rtData.value(RealtimeData::Watts) / 5; // joules
valueLabel->setText(QString("%1").arg(round(sum/1000))); // kJoules
break;
// COGGAN Metrics
case RealtimeData::NP:
case RealtimeData::IF:
case RealtimeData::TSS:
case RealtimeData::VI:
{
// Update sum of watts for last 30 seconds
sum += rtData.value(RealtimeData::Watts);
sum -= rolling[index];
rolling[index] = rtData.value(RealtimeData::Watts);
// raise average to the 4th power
rollingSum += pow(sum/150,4); // raise rolling average to 4th power
count ++;
// move index on/round
index = (index >= 149) ? 0 : index+1;
// calculate NP
double np = pow(rollingSum / (count), 0.25);
if (series == RealtimeData::NP) {
// We only wanted NP so thats it
valueLabel->setText(QString("%1").arg(round(np)));
} else {
double rif, cp;
// carry on and calculate IF
if (mainWindow->zones()) {
// get cp for today
int zonerange = mainWindow->zones()->whichRange(QDateTime::currentDateTime().date());
if (zonerange >= 0) cp = mainWindow->zones()->getCP(zonerange);
else cp = 0;
} else {
cp = 0;
}
if (cp) rif = np / cp;
else rif = 0;
if (series == RealtimeData::IF) {
// we wanted IF so thats it
valueLabel->setText(QString("%1").arg(rif, 0, 'f', 3));
} else {
double normWork = np * (rtData.value(RealtimeData::Time) / 1000); // msecs
double rawTSS = normWork * rif;
double workInAnHourAtCP = cp * 3600;
double tss = rawTSS / workInAnHourAtCP * 100.0;
if (series == RealtimeData::TSS) {
valueLabel->setText(QString("%1").arg(tss, 0, 'f', 1));
} else {
// track average power for VI
apsum += rtData.value(RealtimeData::Watts);
apcount++;
double ap = apsum ? apsum / apcount : 0;
// VI is all that is left!
valueLabel->setText(QString("%1").arg(ap ? np / ap : 0, 0, 'f', 3));
}
}
}
}
break;
// SKIBA Metrics
case RealtimeData::XPower:
case RealtimeData::RI:
case RealtimeData::BikeScore:
case RealtimeData::SkibaVI:
{
static const double exp = 2.0f / ((25.0f / 0.2f) + 1.0f);
static const double rem = 1.0f - exp;
count++;
if (count < 125) {
// get up to speed
rsum += rtData.value(RealtimeData::Watts);
ewma = rsum / count;
} else {
// we're up to speed
ewma = (rtData.value(RealtimeData::Watts) * exp) + (ewma * rem);
}
sum += pow(ewma, 4.0f);
double xpower = pow(sum / count, 0.25f);
if (series == RealtimeData::XPower) {
// We wanted XPower!
valueLabel->setText(QString("%1").arg(round(xpower)));
} else {
double rif, cp;
// carry on and calculate IF
if (mainWindow->zones()) {
// get cp for today
int zonerange = mainWindow->zones()->whichRange(QDateTime::currentDateTime().date());
if (zonerange >= 0) cp = mainWindow->zones()->getCP(zonerange);
else cp = 0;
} else {
cp = 0;
}
if (cp) rif = xpower / cp;
else rif = 0;
if (series == RealtimeData::RI) {
// we wanted IF so thats it
valueLabel->setText(QString("%1").arg(rif, 0, 'f', 3));
} else {
double normWork = xpower * (rtData.value(RealtimeData::Time) / 1000); // msecs
double rawTSS = normWork * rif;
double workInAnHourAtCP = cp * 3600;
double tss = rawTSS / workInAnHourAtCP * 100.0;
if (series == RealtimeData::BikeScore) {
valueLabel->setText(QString("%1").arg(tss, 0, 'f', 1));
} else {
// track average power for Relative Intensity
apsum += rtData.value(RealtimeData::Watts);
apcount++;
double ap = apsum ? apsum / apcount : 0;
// RI is all that is left!
valueLabel->setText(QString("%1").arg(ap ? xpower / ap : 0, 0, 'f', 3));
}
}
}
}
break;
default:
valueLabel->setText(QString("%1").arg(round(value)));
break;
}
}
void DialWindow::resizeEvent(QResizeEvent * )
{
// set point size
int size = geometry().height()-24;
if (size <= 0) size = 4;
if (size >= 64) size = 64;
QFont font;
font.setPointSize(size);
font.setWeight(QFont::Bold);
valueLabel->setFont(font);
}
void DialWindow::seriesChanged()
{
// we got some!
RealtimeData::DataSeries series = static_cast<RealtimeData::DataSeries>
(seriesSelector->itemData(seriesSelector->currentIndex()).toInt());
// the series selector changed so update the colors
switch(series) {
case RealtimeData::Time:
case RealtimeData::LapTime:
case RealtimeData::Distance:
case RealtimeData::Lap:
case RealtimeData::RI:
case RealtimeData::IF:
case RealtimeData::VI:
case RealtimeData::SkibaVI:
case RealtimeData::None:
foreground = GColor(CPLOTMARKER);
break;
case RealtimeData::Load:
case RealtimeData::BikeScore:
case RealtimeData::TSS:
foreground = Qt::blue;
break;
case RealtimeData::XPower:
case RealtimeData::NP:
case RealtimeData::Joules:
case RealtimeData::Watts:
case RealtimeData::AvgWatts:
foreground = GColor(CPOWER);
break;
case RealtimeData::Speed:
case RealtimeData::AvgSpeed:
foreground = GColor(CSPEED);
break;
case RealtimeData::Cadence:
case RealtimeData::AvgCadence:
foreground = GColor(CCADENCE);
break;
case RealtimeData::HeartRate:
case RealtimeData::AvgHeartRate:
foreground = GColor(CHEARTRATE);
break;
}
// ugh. we use style sheets becuase palettes don't work on labels
background = GColor(CRIDEPLOTBACKGROUND);
setProperty("color", background);
QString sh = QString("QLabel { background: %1; color: %2; }")
.arg(background.name())
.arg(foreground.name());
valueLabel->setStyleSheet(sh);
}