diff --git a/src/WorkoutWidget.cpp b/src/WorkoutWidget.cpp index 503bc52bd..28fce397b 100644 --- a/src/WorkoutWidget.cpp +++ b/src/WorkoutWidget.cpp @@ -183,6 +183,9 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event) if (state == none) { updateNeeded = createPoint(p); + // recompute metrics + recompute(); + // but we may press and hold for a snip // so lets set the timer and remember // where we were @@ -208,6 +211,9 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event) state = none; dragging = NULL; updateNeeded = true; + + // recompute metrics + recompute(); } // @@ -221,7 +227,7 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event) } // - // 5. MOUSE WHEEL + // 5. MOUSE WHEEL // if (event->type() == QEvent::Wheel) { @@ -236,6 +242,9 @@ WorkoutWidget::eventFilter(QObject *obj, QEvent *event) returning = true; } + // will need to .. + recompute(); + } // ALL DONE @@ -322,24 +331,32 @@ WorkoutWidget::createPoint(QPoint p) // add a point! QPointF to = reverseTransform(p.x(), p.y()); - // don't auto append, we are going to insert vvvvv - dragging = new WWPoint(this, to.x(), to.y(), false); - state = drag; + // don't auto append, we are going to insert vvvvv + WWPoint *add = new WWPoint(this, to.x(), to.y(), false); - onDrag = QPointF(dragging->x, dragging->y); // yuk, this should be done in the FSM (eventFilter) - // action at a distance ... yuk XXX TIDY THIS XXX + onDrag = QPointF(add->x, add->y); // yuk, this should be done in the FSM (eventFilter) + // action at a distance ... yuk XXX TIDY THIS XXX // add into the points for(int i=0; ix > to.x()) { - points_.insert(i, dragging); + points_.insert(i, add); new CreatePointCommand(this, to.x(), to.y(), i); + + // enter drag mode -- add command resets we + // are an edge case so handle it ourselves + state = drag; + dragging = add; return true; } } // after current - points_.append(dragging); + points_.append(add); new CreatePointCommand(this, to.x(), to.y(), -1); + + // enter drag mode - edge case as above + state = drag; + dragging = add; return true; } @@ -361,7 +378,7 @@ WorkoutWidget::scale(QPoint p) return true; } -void +void WorkoutWidget::ergFileSelected(ErgFile *ergFile) { // reset state and stack @@ -393,9 +410,159 @@ WorkoutWidget::ergFileSelected(ErgFile *ergFile) maxY_ *= 1.1f; } + + // reset metrics etc + recompute(); + + // repaint repaint(); } +void +WorkoutWidget::recompute() +{ + //QTime timer; + //timer.start(); + + int rnum=-1; + if (context->athlete->zones(false) == NULL || + (rnum = context->athlete->zones(false)->whichRange(QDate::currentDate())) == -1) { + + // no cp or ftp set + parent->TSSlabel->setText("- TSS"); + parent->IFlabel->setText("- IF"); + + } + + // get CP/FTP to use in calculation + int CP = context->athlete->zones(false)->getCP(rnum); + int FTP = context->athlete->zones(false)->getFTP(rnum); + bool useCPForFTP = (appsettings->cvalue(context->athlete->cyclist, + context->athlete->zones(false)->useCPforFTPSetting(), 0).toInt() == 0); + if (useCPForFTP) FTP=CP; + + // compute the metrics based upon the data... + static const int SAMPLERATE=1000;// in ms + QVector wattsArray; + + // start from 0s + int lastsecs = 0; + + // running time, in seconds + int time = 0; + + // aggregating + int ssecs = 0; + int sjoules=0; + + // resample the erg file into 1s samples + foreach(WWPoint *p, points_) { + + // delta in ms + int dt = (p->x - lastsecs) * SAMPLERATE; + int watts = p->y; + + // + // AGGREGATE INTO SAMPLES + // + while (dt > 0) { + + // we keep track of how much time has been aggregated + // into sample, so 'need' is whats left to aggregate + // for the full sample + int need = SAMPLERATE - ssecs; + + // aggregate + if (dt < need) { + + // the entire sample read is less than we need + // so aggregate the whole lot and wait fore more + // data to be read. If there is no more data then + // this will be lost, we don't keep incomplete samples + ssecs += dt; + sjoules += dt * watts; + dt = 0; + + } else { + + // dt is more than we need to fill and entire sample + // so lets just take the fraction we need + dt -= need; + + // accumulating time and distance + time += double(SAMPLERATE) / 1000.0f; + + // averaging sample data + sjoules += need * watts; + sjoules /= double(SAMPLERATE); + + // add to array + wattsArray.append(sjoules); + + // the next sample + ssecs = 0; + sjoules = 0; + } + } + lastsecs = p->x; + } + + // + // Now we have an array we can compute metrics from it + // we compute locally for speed rather than use the + // metric compute functions .. might change this if + // it can be done in a performant manner. + // + // The Workout Window has labels for TSS and IF. + double NP=0, TSS=0, IF=0; + + // calculating NP + QVector NProlling(30); + NProlling.fill(0,30); + double NPtotal=0; + double NPsum=0; + int NPindex=0; + int NPcount=0; + + foreach(int watts, wattsArray) { + + // + // Normalised Power + // + + // sum last 30secs + NPsum += watts; + NPsum -= NProlling[NPindex]; + NProlling[NPindex] = watts; + + // running total and count + NPtotal += pow(NPsum/30,4); // raise rolling average to 4th power + NPcount ++; + + // it moves up and down during the ride + if (NPcount > 30) { + NP = pow(double(NPtotal) / double(NPcount), 0.25f); + } + + // move index on/round + NPindex = (NPindex >= 29) ? 0 : NPindex+1; + } + + // IF..... + IF = double(NP) / double(FTP); + + // TSS..... + double normWork = NP * NPcount; + double rawTSS = normWork * IF; + double workInAnHourAtCP = FTP * 3600; + TSS = rawTSS / workInAnHourAtCP * 100.0; + + parent->IFlabel->setText(QString("%1 IF").arg(IF, 0, 'f', 2)); + parent->TSSlabel->setText(QString("%1 TSS").arg(TSS, 0, 'f', 0)); + + //qDebug()<<"RECOMPUTE:"< 0) parent->undoAct->setEnabled(true); if (stackptr >= stack.count()) parent->redoAct->setEnabled(false); + // recompute metrics + recompute(); + + // update update(); } @@ -677,5 +849,9 @@ WorkoutWidget::undo() if (stackptr <= 0) parent->undoAct->setEnabled(false); if (stackptr < stack.count()) parent->redoAct->setEnabled(true); + // recompute metrics + recompute(); + + // update update(); } diff --git a/src/WorkoutWidget.h b/src/WorkoutWidget.h index ea1ce5081..68c01158c 100644 --- a/src/WorkoutWidget.h +++ b/src/WorkoutWidget.h @@ -130,6 +130,9 @@ class WorkoutWidget : public QWidget // and erg file was selected void ergFileSelected(ErgFile *); + // recompute metrics etc + void recompute(); + // trap signals void configChanged(qint32); diff --git a/src/WorkoutWindow.cpp b/src/WorkoutWindow.cpp index 5d068e193..fe828d84b 100644 --- a/src/WorkoutWindow.cpp +++ b/src/WorkoutWindow.cpp @@ -96,6 +96,12 @@ WorkoutWindow::WorkoutWindow(Context *context) : ylabel = new QLabel("150w"); toolbar->addWidget(ylabel); + IFlabel = new QLabel("0 IF"); + toolbar->addWidget(IFlabel); + + TSSlabel = new QLabel("0 TSS"); + toolbar->addWidget(TSSlabel); + #if 0 // not yet! // get updates.. connect(context, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData))); @@ -120,6 +126,10 @@ WorkoutWindow::configChanged(qint32) QFontMetrics fm(workout->bigFont); xlabel->setFont(workout->bigFont); ylabel->setFont(workout->bigFont); + IFlabel->setFont(workout->bigFont); + TSSlabel->setFont(workout->bigFont); + IFlabel->setFixedWidth(fm.boundingRect(" 0.85 IF ").width()); + TSSlabel->setFixedWidth(fm.boundingRect(" 100 TSS ").width()); xlabel->setFixedWidth(fm.boundingRect(" 00:00:00 ").width()); ylabel->setFixedWidth(fm.boundingRect(" 1000w ").width()); @@ -127,6 +137,8 @@ WorkoutWindow::configChanged(qint32) .arg(GCColor::invertColor(GColor(CPLOTBACKGROUND)).name())); xlabel->setStyleSheet("color: darkGray;"); ylabel->setStyleSheet("color: darkGray;"); + TSSlabel->setStyleSheet("color: darkGray;"); + IFlabel->setStyleSheet("color: darkGray;"); repaint(); } diff --git a/src/WorkoutWindow.h b/src/WorkoutWindow.h index 843dd08ac..b88c50071 100644 --- a/src/WorkoutWindow.h +++ b/src/WorkoutWindow.h @@ -49,6 +49,8 @@ class WorkoutWindow : public GcWindow // workout widget updates these QLabel *xlabel, *ylabel; + QLabel *TSSlabel, *IFlabel; + QAction *saveAct, *undoAct, *redoAct, *drawAct, *selectAct;