mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
The slider in the controls should only be shown for HR/Power/Cadence. Additionally, it is more of a 'smoothing' (rolling) average, so the text has been changed to reflect that. This is to avoid confusion with the other series entitled Average Power. Average Cadence and Average heart rate - which are averages for the entire ride.
512 lines
14 KiB
C++
512 lines
14 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), average(1)
|
|
{
|
|
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);
|
|
|
|
// average selection
|
|
averageLabel= new QLabel(tr("Smooth (secs)"), this);
|
|
averageLabel->hide();
|
|
averageLabel->setAutoFillBackground(true);
|
|
|
|
QHBoxLayout *averageLayout = new QHBoxLayout;
|
|
averageSlider = new QSlider(Qt::Horizontal);
|
|
averageSlider->hide();
|
|
averageSlider->setTickPosition(QSlider::TicksBelow);
|
|
averageSlider->setTickInterval(5);
|
|
averageSlider->setMinimum(1);
|
|
averageSlider->setMaximum(30);
|
|
averageSlider->setValue(1);
|
|
averageLayout->addWidget(averageSlider);
|
|
averageEdit = new QLineEdit();
|
|
averageEdit->hide();
|
|
averageEdit->setFixedWidth(20);
|
|
averageEdit->setText(QString("%1").arg(1) );
|
|
averageLayout->addWidget(averageEdit);
|
|
|
|
controlsLayout->addRow(averageLabel, averageLayout);
|
|
|
|
// 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(averageSlider, SIGNAL(valueChanged(int)),this, SLOT(setAverageFromSlider()));
|
|
connect(averageEdit, SIGNAL(textChanged(const QString)), this, SLOT(setAverageFromText(const QString)));
|
|
|
|
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);
|
|
|
|
// Average value for display for HeartRate, Watts and Cadence
|
|
double displayValue = value;
|
|
|
|
if (series == RealtimeData::HeartRate ||
|
|
series == RealtimeData::Watts ||
|
|
series == RealtimeData::Cadence) {
|
|
|
|
sum += value;
|
|
int j = index-(count<average*5?count:average*5);
|
|
sum -= rolling[(j>=0?j:150+j)];
|
|
|
|
//store value
|
|
rolling[index] = value;
|
|
|
|
// move index on/round for next value
|
|
index = (index >= 149) ? 0 : index+1;
|
|
count++;
|
|
|
|
if (average > 1) {
|
|
// rolling average
|
|
if (count < average*5)
|
|
displayValue = sum/(count);
|
|
else
|
|
displayValue = sum/(average*5);
|
|
}
|
|
|
|
}
|
|
|
|
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:
|
|
case RealtimeData::VirtualSpeed:
|
|
if (!mainWindow->useMetricUnits) displayValue *= MILES_PER_KM;
|
|
valueLabel->setText(QString("%1").arg(value, 0, 'f', 1));
|
|
break;
|
|
|
|
case RealtimeData::Distance:
|
|
if (!mainWindow->useMetricUnits) displayValue *= 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;
|
|
|
|
case RealtimeData::Load:
|
|
if (rtData.mode == ERG || rtData.mode == MRC) {
|
|
value = rtData.getLoad();
|
|
valueLabel->setText(QString("%1").arg(round(value)));
|
|
} else {
|
|
value = rtData.getSlope();
|
|
valueLabel->setText(QString("%1%").arg(value, 0, 'f', 1));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
valueLabel->setText(QString("%1").arg(round(displayValue)));
|
|
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());
|
|
|
|
if (series == RealtimeData::HeartRate ||
|
|
series == RealtimeData::Watts ||
|
|
series == RealtimeData::Cadence) {
|
|
averageLabel->show();
|
|
averageEdit->show();
|
|
averageSlider->show();
|
|
} else {
|
|
averageLabel->hide();
|
|
averageEdit->hide();
|
|
averageSlider->hide();
|
|
}
|
|
|
|
// 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::VirtualSpeed:
|
|
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);
|
|
}
|
|
|
|
void
|
|
DialWindow::setAverageFromText(const QString text) {
|
|
int value = text.toDouble();
|
|
averageEdit->setText(text);
|
|
if (average != value) {
|
|
average = value;
|
|
averageSlider->setValue(average);
|
|
|
|
// correction of sum
|
|
sum = 0;
|
|
int j = index-(count<average*5?count:average*5);
|
|
for (int i=0; i<average*5; i++) {
|
|
sum += rolling[(j>=0?j:150+j)];
|
|
j++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
DialWindow::setAverageFromSlider() {
|
|
if (average != averageSlider->value()) {
|
|
setAverageFromText(QString("%1").arg(averageSlider->value()));
|
|
}
|
|
}
|