/* * 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(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 (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 (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); }