/* * 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), isNewLap(false) { 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(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())); connect(mainWindow, SIGNAL(newLap()), this, SLOT(onNewLap())); // 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 (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::AltWatts || series == RealtimeData::Cadence) { sum += value; int j = index-(count=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); } } if( isNewLap && ( series == RealtimeData::AvgCadenceLap || series == RealtimeData::AvgHeartRateLap || series == RealtimeData::AvgSpeedLap || series == RealtimeData::AvgWattsLap ) ) { count = 0; sum = 0.0; isNewLap = false; } 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::LapTimeRemaining: { long msecs = value; valueLabel->setText(QString("%1:%2:%3").arg(msecs/3600000) .arg((msecs%3600000)/60000,2,10,QLatin1Char('0')) .arg((msecs%60000)/1000,2,10,QLatin1Char('0'))); } break; case RealtimeData::LRBalance: { double tot = rtData.getWatts() + rtData.getAltWatts(); double left = rtData.getWatts() / tot * 100.00f; double right = 100.00 - left; if (tot < 0.1) left = right = 0; valueLabel->setText(QString("%1 / %2").arg(left, 0, 'f', 0).arg(right, 0, 'f', 0)); } break; case RealtimeData::Speed: case RealtimeData::VirtualSpeed: 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: case RealtimeData::AvgWattsLap: sum += rtData.value(RealtimeData::Watts); count++; value = sum / count; valueLabel->setText(QString("%1").arg(round(value))); break; case RealtimeData::AvgSpeed: case RealtimeData::AvgSpeedLap: 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: case RealtimeData::AvgCadenceLap: sum += rtData.value(RealtimeData::Cadence); count++; value = sum / count; valueLabel->setText(QString("%1").arg(round(value))); break; case RealtimeData::AvgHeartRate: case RealtimeData::AvgHeartRateLap: 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 (seriesSelector->itemData(seriesSelector->currentIndex()).toInt()); if (series == RealtimeData::HeartRate || series == RealtimeData::Watts || series == RealtimeData::AltWatts || 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::LapTimeRemaining: case RealtimeData::Distance: case RealtimeData::LRBalance: case RealtimeData::Lap: case RealtimeData::RI: case RealtimeData::IF: case RealtimeData::VI: case RealtimeData::SkibaVI: case RealtimeData::None: foreground = GColor(CDIAL); break; case RealtimeData::Load: foreground = GColor(CLOAD); break; case RealtimeData::BikeScore: foreground = GColor(CBIKESCORE); break; case RealtimeData::TSS: foreground = GColor(CTSS); break; case RealtimeData::XPower: case RealtimeData::NP: case RealtimeData::Joules: case RealtimeData::Watts: case RealtimeData::AvgWatts: case RealtimeData::AvgWattsLap: foreground = GColor(CPOWER); break; case RealtimeData::Speed: case RealtimeData::VirtualSpeed: case RealtimeData::AvgSpeed: case RealtimeData::AvgSpeedLap: foreground = GColor(CSPEED); break; case RealtimeData::Cadence: case RealtimeData::AvgCadence: case RealtimeData::AvgCadenceLap: foreground = GColor(CCADENCE); break; case RealtimeData::HeartRate: case RealtimeData::AvgHeartRate: case RealtimeData::AvgHeartRateLap: foreground = GColor(CHEARTRATE); break; case RealtimeData::AltWatts: foreground = GColor(CALTPOWER); 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=0?j:150+j)]; j++; } } } void DialWindow::setAverageFromSlider() { if (average != averageSlider->value()) { setAverageFromText(QString("%1").arg(averageSlider->value())); } } void DialWindow::onNewLap() { isNewLap = true; }