diff --git a/src/ErgFile.h b/src/ErgFile.h index 9a39dfa6c..8f2c39898 100644 --- a/src/ErgFile.h +++ b/src/ErgFile.h @@ -111,8 +111,9 @@ class ErgFile double XP, RI, BS, SVI; // Skiba for erg / mrc double ELE, ELEDIST, GRADE; // crs - private: Context *context; + + private: int &mode; int nomode; }; diff --git a/src/ErgFilePlot.cpp b/src/ErgFilePlot.cpp index 4e0d6df3e..e94847e65 100644 --- a/src/ErgFilePlot.cpp +++ b/src/ErgFilePlot.cpp @@ -18,6 +18,7 @@ #include "ErgFilePlot.h" +#include "WPrime.h" #include "Context.h" // Bridge between QwtPlot and ErgFile to avoid having to @@ -89,6 +90,8 @@ ErgFilePlot::ErgFilePlot(Context *context) : context(context) setCanvasBackground(GColor(CRIDEPLOTBACKGROUND)); static_cast(canvas())->setFrameStyle(QFrame::NoFrame); //courseData = data; // what we plot + setAutoDelete(false); + setAxesCount(QwtAxis::yRight, 4); // Setup the left axis (Power) setAxisTitle(yLeft, "Watts"); @@ -158,6 +161,31 @@ ErgFilePlot::ErgFilePlot(Context *context) : context(context) QPen Lodpen = QPen(Qt::blue, 1.0); LodCurve->setPen(Lodpen); + wbalCurvePredict = new QwtPlotCurve("W'bal Predict"); + wbalCurvePredict->attach(this); + wbalCurvePredict->setYAxis(QwtAxisId(QwtAxis::yRight, 3)); + QColor predict = GColor(CWBAL).darker(); + predict.setAlpha(200); + QPen wbalPen = QPen(predict, 2.0); // predict darker... + wbalCurvePredict->setPen(wbalPen); + wbalCurvePredict->setVisible(true); + + wbalCurveActual = new QwtPlotCurve("W'bal Actual"); + wbalCurveActual->attach(this); + wbalCurveActual->setYAxis(QwtAxisId(QwtAxis::yRight, 3)); + QPen wbalPenA = QPen(GColor(CWBAL), 1.0); // actual lighter + wbalCurveActual->setPen(wbalPenA); + + sd = new QwtScaleDraw; + sd->enableComponent(QwtScaleDraw::Ticks, false); + sd->enableComponent(QwtScaleDraw::Backbone, false); + sd->setLabelRotation(90);// in the 000s + sd->setTickLength(QwtScaleDiv::MajorTick, 3); + setAxisScaleDraw(QwtAxisId(QwtAxis::yRight, 3), sd); + pal.setColor(QPalette::WindowText, GColor(CWBAL)); + pal.setColor(QPalette::Text, GColor(CWBAL)); + axisWidget(QwtAxisId(QwtAxis::yRight, 3))->setPalette(pal); + // telemetry history wattsCurve = new QwtPlotCurve("Power"); QPen wattspen = QPen(GColor(CPOWER)); @@ -339,6 +367,21 @@ ErgFilePlot::setData(ErgFile *ergfile) } } + // wbal predict curve and clear actual curve + QVector empty; + wbalCurveActual->setSamples(empty, empty); + + // compute wbal curve for the erg file + calculator; + calculator.setErg(ergfile); + + setAxisTitle(QwtAxisId(QwtAxis::yRight, 3), tr("W' Balance (j)")); + setAxisScale(QwtAxisId(QwtAxis::yRight, 3),calculator.minY-1000,calculator.maxY+1000); + setAxisLabelAlignment(QwtAxisId(QwtAxis::yRight, 3),Qt::AlignVCenter); + + // and the values ... but avoid sharing! + wbalCurvePredict->setSamples(calculator.xdata(), calculator.ydata()); + } else { // clear the plot we have nothing selected diff --git a/src/ErgFilePlot.h b/src/ErgFilePlot.h index 800a6642c..410c54300 100644 --- a/src/ErgFilePlot.h +++ b/src/ErgFilePlot.h @@ -36,6 +36,7 @@ #include #include #include "ErgFile.h" +#include "WPrime.h" #include "Settings.h" #include "Colors.h" @@ -44,6 +45,7 @@ #include #include +#define DEFAULT_TAU 450 class ErgFileData : public QwtPointArrayData { @@ -143,12 +145,15 @@ class ErgFilePlot : public QwtPlot private: + WPrime calculator; Context *context; bool bydist; ErgFile *ergFile; QwtPlotGrid *grid; QwtPlotCurve *LodCurve; + QwtPlotCurve *wbalCurveActual; + QwtPlotCurve *wbalCurvePredict; QwtPlotCurve *wattsCurve; QwtPlotCurve *hrCurve; QwtPlotCurve *cadCurve; diff --git a/src/WPrime.cpp b/src/WPrime.cpp index e0d582643..1c5471c55 100644 --- a/src/WPrime.cpp +++ b/src/WPrime.cpp @@ -290,6 +290,174 @@ WPrime::setRide(RideFile *input) } } +void +WPrime::setErg(ErgFile *input) +{ + QTime time; // for profiling performance of the code + time.start(); + + // reset from previous + values.resize(0); // the memory is kept for next time so this is efficient + xvalues.resize(0); + + EXP = PCP_ = 0; + + // Get CP + CP = 250; // defaults + WPRIME = 20000; + CP=250; + TAU=545; + + if (input->context->athlete->zones()) { + int zoneRange = input->context->athlete->zones()->whichRange(QDate::currentDate()); + CP = zoneRange >= 0 ? input->context->athlete->zones()->getCP(zoneRange) : 250; + WPRIME = zoneRange >= 0 ? input->context->athlete->zones()->getWprime(zoneRange) : 20000; + } + + // no data or no power data then forget it. + bool bydist = (input->format == CRS) ? true : false; + if (!input->isValid() || bydist) { + return; // needs to be a valid erg file... + } + + minY = maxY = WPRIME; + last = input->Duration / 1000; + + + // input array contains the actual W' expenditure + // and will also contain non-zero values + double totalBelowCP=0; + double countBelowCP=0; + QVector inputArray(last+1); + EXP = 0; + for (int i=0; iwattsAt(i*1000, lap); + + inputArray[i] = value > CP ? value-CP : 0; + + if (value < CP) { + totalBelowCP += value; + countBelowCP++; + } else EXP += value; // total expenditure above CP + } + + // STEP 2: ITERATE OVER DATA TO CREATE W' DATA SERIES + + // lets run forward from 0s to end of ride + minY = WPRIME; + maxY = WPRIME; + values.resize(last+1); + xvalues.resize(last+1); + + QVector myvalues(last+1); + + int stop = last / 2; + + WPrimeIntegrator a(inputArray, 0, stop, TAU); + WPrimeIntegrator b(inputArray, stop+1, last, TAU); + + a.start(); + b.start(); + + a.wait(); + b.wait(); + + // sum values + for (int t=0; t<=last; t++) { + values[t] = a.output[t] + b.output[t]; + xvalues[t] = t * 1000; + } + + // now subtract WPRIME and work out minimum etc + for(int t=0; t <= last; t++) { + double value = WPRIME - values[t]; + values[t] = value; + + if (value > maxY) maxY = value; + if (value < minY) minY = value; + } + + // STEP 3: FIND MATCHES + + // SMOOTH DATA SERIES + + // get raw data adjusted to 1s intervals (as before) + QVector smoothArray(last+1); + QVector rawArray(last+1); + for (int i=0; i0 && last-i >=0; i--) { + rtot += smoothArray[last-i]; + } + + // now run backwards setting the rolling average + for (int i=last; i>=WprimeMatchSmoothing; i--) { + int here = smoothArray[i]; + smoothArray[i] = rtot / WprimeMatchSmoothing; + rtot -= here; + rtot += smoothArray[i-WprimeMatchSmoothing]; + } + + // FIND MATCHES -- INTERVALS WHERE POWER > CP + // AND W' DEPLETED BY > WprimeMatchMinJoules + bool inmatch=false; + matches.clear(); + mvalues.clear(); + mxvalues.clear(); + for(int i=0; i= CP || rawArray[i] >= CP)) { + inmatch=true; + match.start=i; + } + + if (inmatch && (smoothArray[i] < CP && rawArray[i] < CP)) { + + // lets work backwards as we're at the end + // we only care about raw data to avoid smoothing + // artefacts + int end=i-1; + while (end > match.start && rawArray[end] < CP) { + end--; + } + + if (end > match.start) { + + match.stop = end; + match.secs = (match.stop-match.start) +1; // don't fencepost! + match.cost = values[match.start] - values[match.stop]; + + if (match.cost >= WprimeMatchMinJoules) { + matches << match; + } + } + inmatch=false; + } + } + + // SET MATCH SERIES FOR ALLPLOT CHART + foreach (struct Match match, matches) { + + // we only count 1kj asa match + if (match.cost >= 2000) { //XXX need to agree how to define a match -- or even if we want to... + mvalues << values[match.start]; + mxvalues << xvalues[match.start]; + mvalues << values[match.stop]; + mxvalues << xvalues[match.stop]; + } + } +} + double WPrime::PCP() { @@ -367,7 +535,6 @@ WPrime::minForCP(int cp) if (value < min) min = value; } //qDebug()<<"compute time="<