diff --git a/src/BikeScore.cpp b/src/BikeScore.cpp index 850d68ac3..f0429f282 100644 --- a/src/BikeScore.cpp +++ b/src/BikeScore.cpp @@ -181,6 +181,48 @@ class RelativeIntensity : public RideMetric { RideMetric *clone() const { return new RelativeIntensity(*this); } }; +class CriticalPower : public RideMetric { + Q_DECLARE_TR_FUNCTIONS(CriticalPower) + + public: + + CriticalPower() + { + setSymbol("cp_setting"); + setInternalName("CP setting"); + } + void initialize() { + setName(tr("Critical Power")); + setType(RideMetric::Average); + setMetricUnits(tr("")); + setImperialUnits(tr("")); + setPrecision(0); + } + void compute(const RideFile *r, const Zones *zones, int zoneRange, + const HrZones *, int, + const QHash &, + const Context *) { + + // did user override for this ride? + int cp = r->getTag("CP","0").toInt(); + + // not overriden so use the set value + // if it has been set at all + if (!cp && zones && zoneRange >= 0) + cp = zones->getCP(zoneRange); + + setValue(cp); + } + + bool canAggregate() { return true; } + void aggregateWith(const RideMetric &other) { + assert(symbol() == other.symbol()); + setValue(other.value(true) > value(true) ? other.value(true) : value(true)); + } + + RideMetric *clone() const { return new CriticalPower(*this); } +}; + class BikeScore : public RideMetric { Q_DECLARE_TR_FUNCTIONS(BikeScore) double score; @@ -258,7 +300,8 @@ class ResponseIndex : public RideMetric { RideMetric *clone() const { return new ResponseIndex(*this); } }; -static bool addAllFive() { +static bool addAllSix() { + RideMetricFactory::instance().addMetric(CriticalPower()); RideMetricFactory::instance().addMetric(XPower()); QVector deps; deps.append("skiba_xpower"); @@ -276,5 +319,5 @@ static bool addAllFive() { return true; } -static bool allFiveAdded = addAllFive(); +static bool allSixAdded = addAllSix(); diff --git a/src/DBAccess.cpp b/src/DBAccess.cpp index bf03c0334..29b7516f5 100644 --- a/src/DBAccess.cpp +++ b/src/DBAccess.cpp @@ -78,8 +78,9 @@ // 57 20 Jan 2014 Mark Liversedge Added W' Expenditure for total energy spent above CP // 58 23 Jan 2014 Mark Liversedge W' work rename and calculate without reference to WPrime class (speed) // 59 24 Jan 2014 Mark Liversedge Added Maximum W' exp which is same as W'bal bur expressed as used not left +// 60 05 Feb 2014 Mark Liversedge Added Critical Power as a metric -- retreives from settings for now -int DBSchemaVersion = 59; +int DBSchemaVersion = 60; DBAccess::DBAccess(Context* context) : context(context), db(NULL) { diff --git a/src/LTMPlot.cpp b/src/LTMPlot.cpp index 3ba1a9124..6a72e17dc 100644 --- a/src/LTMPlot.cpp +++ b/src/LTMPlot.cpp @@ -27,6 +27,7 @@ #include "MetricAggregator.h" #include "SummaryMetrics.h" #include "RideMetric.h" +#include "RideFileCache.h" #include "Settings.h" #include "Colors.h" @@ -303,9 +304,9 @@ LTMPlot::setData(LTMSettings *set) int count; if (settings->groupBy != LTM_TOD) - createCurveData(settings, metricDetail, *xdata, *ydata, count); + createCurveData(context, settings, metricDetail, *xdata, *ydata, count); else - createTODCurveData(settings, metricDetail, *xdata, *ydata, count); + createTODCurveData(context, settings, metricDetail, *xdata, *ydata, count); // we add in the last curve for X axis values if (r) { @@ -499,9 +500,9 @@ LTMPlot::setData(LTMSettings *set) int count; if (settings->groupBy != LTM_TOD) - createCurveData(settings, metricDetail, xdata, ydata, count); + createCurveData(context, settings, metricDetail, xdata, ydata, count); else - createTODCurveData(settings, metricDetail, xdata, ydata, count); + createTODCurveData(context, settings, metricDetail, xdata, ydata, count); //qDebug()<<"Create curve data.."<&x,QVector&y,int&n) +LTMPlot::setCompareData(LTMSettings *set) +{ + QTime timer; + timer.start(); + + double MAXX=0.0; // maximum value for x, always from 0-n + + //qDebug()<<"Starting.."< c(curves); + while (c.hasNext()) { + c.next(); + QString symbol = c.key(); + QwtPlotCurve *current = c.value(); + //current->detach(); // the destructor does this for you + delete current; + } + curves.clear(); + if (highlighter) { + highlighter->detach(); + delete highlighter; + highlighter = NULL; + } + foreach (QwtPlotMarker *label, labels) { + label->detach(); + delete label; + } + labels.clear(); + + // disable all y axes until we have populated + for (int i=0; i<8; i++) { + setAxisVisible(supportedAxes[i], false); + enableAxis(supportedAxes[i].id, false); + } + axes.clear(); + + // reset all min/max Y values + for (int i=0; i<10; i++) minY[i]=0, maxY[i]=0; + + // which yAxis did we use (should be yLeft) + QwtAxisId axisid(QwtPlot::yLeft, 0); + + // which compare date range are we on? + int cdCount =0; + + // how many bars? + int bars =0; + foreach(CompareDateRange cd, context->compareDateRanges) if (cd.checked) bars++; + + // + // Setup curve for every Date Range being plotted + // + foreach(CompareDateRange cd, context->compareDateRanges) { + + // only plot date ranges selected! + if (!cd.checked) continue; + + // increment count of date ranges we have + cdCount++; + + //QColor color; + //QDate start, end; + //int days; + //Context *sourceContext; + + // wipe away last cached stress calculator -- it gets redone for each curve + // so pretty slow sadly + if (cogganPMC) { delete cogganPMC; cogganPMC=NULL; } + if (skibaPMC) { delete skibaPMC; skibaPMC=NULL; } + + settings = set; + settings->start = QDateTime(cd.start, QTime()); + settings->end = QDateTime(cd.end, QTime()); + + // For each metric in chart, translate units and name if default uname + //XXX BROKEN XXX LTMTool::translateMetrics(context, settings); + + // set the settings data source to the compare date range + // QList metrics, measures; + settings->data = &cd.metrics; + settings->measures = &cd.measures; + + // we need to do this for each date range as they are dependant + // on the metrics chosen and can't be pre-cached + QList herebests; + herebests = RideFileCache::getAllBestsFor(cd.sourceContext, settings->metrics, settings->start, settings->end); + settings->bests = &herebests; + + // no data to display so that all folks + if (settings->data == NULL || (*settings->data).count() == 0) continue; + + // crop dates to at least within a year of the data available, but only if we have some data + if (settings->data != NULL && (*settings->data).count() != 0) { + + // end + if (settings->end == QDateTime() || + settings->end > (*settings->data).last().getRideDate().addDays(365)) { + if (settings->end < QDateTime::currentDateTime()) { + settings->end = QDateTime::currentDateTime(); + } else { + settings->end = (*settings->data).last().getRideDate(); + } + } + + // start + if (settings->start == QDateTime() || + settings->start < (*settings->data).first().getRideDate().addDays(-365)) { + settings->start = (*settings->data).first().getRideDate(); + } + } + + switch (settings->groupBy) { + case LTM_TOD: + setAxisTitle(xBottom, tr("Time of Day")); + break; + case LTM_DAY: + setAxisTitle(xBottom, tr("Day")); + break; + case LTM_WEEK: + setAxisTitle(xBottom, tr("Week")); + break; + case LTM_MONTH: + setAxisTitle(xBottom, tr("Month")); + break; + case LTM_YEAR: + setAxisTitle(xBottom, tr("Year")); + break; + default: + setAxisTitle(xBottom, tr("Date")); + break; + } + enableAxis(QwtAxis::xBottom, true); + setAxisVisible(QwtAxis::xBottom, true); + setAxisVisible(QwtAxis::xTop, false); + + + //qDebug()<<"Wiped previous.."<*p, stackX) delete p; + foreach(QVector*q, stackY) delete q; + stackX.clear(); + stackY.clear(); + stacks.clear(); + + int r=0; + foreach (MetricDetail metricDetail, settings->metrics) { + if (metricDetail.stack == true) { + + // register this data + QVector *xdata = new QVector(); + QVector *ydata = new QVector(); + stackX.append(xdata); + stackY.append(ydata); + + int count; + if (settings->groupBy != LTM_TOD) + createCurveData(cd.sourceContext, settings, metricDetail, *xdata, *ydata, count); + else + createTODCurveData(cd.sourceContext, settings, metricDetail, *xdata, *ydata, count); + + // lets catch the x-scale + if (count > MAXX) MAXX=count; + + // we add in the last curve for X axis values + if (r) { + aggregateCurves(*stackY[r], *stackY[r-1]); + } + r++; + } + } + + //qDebug()<<"Created curve data.."<value(this, GC_LINEWIDTH, 2.0).toDouble(); + + // now we iterate over the metric details AGAIN + // but this time in reverse and only plot the + // stacked values. This is because we overcome the + // lack of a stacked plot in QWT by painting decreasing + // bars, with the values aggregated previously + // so if we plot L1 time in zone 1hr and L2 time in zone 1hr + // it plots as L2 time in zone 2hr and then paints over that + // with a L1 time in zone of 1hr. + // + // The tooltip has to unpick the aggregation to ensure + // that it subtracts other data series in the stack from + // the value plotted... all nasty but heck, it works + int stackcounter = stackX.size()-1; + for (int m=settings->metrics.count()-1; m>=0; m--) { + + // + // *ONLY* PLOT STACKS + // + + int count=0; + MetricDetail metricDetail = settings->metrics[m]; + + if (metricDetail.stack == false) continue; + + QVector xdata, ydata; + + // use the aggregated values + xdata = *stackX[stackcounter]; + ydata = *stackY[stackcounter]; + stackcounter--; + count = xdata.size()-2; + + // no data to plot! + if (count <= 0) continue; + + // name is year and metric + QString name = QString ("%1 %2").arg(cd.name).arg(metricDetail.uname); + + // Create a curve + QwtPlotCurve *current = new QwtPlotCurve(name); + if (metricDetail.type == METRIC_BEST) + curves.insert(name, current); + else + curves.insert(name, current); + stacks.insert(current, stackcounter+1); + if (appsettings->value(this, GC_ANTIALIAS, false).toBool() == true) + current->setRenderHint(QwtPlotItem::RenderAntialiased); + QPen cpen = QPen(cd.color); + cpen.setWidth(width); + current->setPen(cpen); + current->setStyle(metricDetail.curveStyle); + + // choose the axis + axisid = chooseYAxis(metricDetail.uunits); + current->setYAxis(axisid); + + // left and right offset for bars + double left = 0; + double right = 0; + + if (metricDetail.curveStyle == QwtPlotCurve::Steps) { + + int barn = cdCount-1; + + double space = double(0.9) / bars; + double gap = space * 0.10; + double width = space * 0.90; + left = (space * barn) + (gap / 2) + 0.1; + right = left + width; + + //left -= 0.5 + gap; + //right -= 0.5 + gap; + } + + if (metricDetail.curveStyle == QwtPlotCurve::Steps) { + + // fill the bars + QColor merge; + merge.setRed((metricDetail.penColor.red() + cd.color.red()) / 2); + merge.setGreen((metricDetail.penColor.green() + cd.color.green()) / 2); + merge.setBlue((metricDetail.penColor.blue() + cd.color.blue()) / 2); + + QColor brushColor = merge; + if (metricDetail.stack == true) { + brushColor.setAlpha(255); + QBrush brush = QBrush(brushColor); + current->setBrush(brush); + } else { + brushColor.setAlpha(64); // now side by side, less transparency required + QColor brushColor1 = brushColor.darker(); + + QLinearGradient linearGradient(0, 0, 0, height()); + linearGradient.setColorAt(0.0, brushColor1); + linearGradient.setColorAt(1.0, brushColor); + linearGradient.setSpread(QGradient::PadSpread); + current->setBrush(linearGradient); + } + + current->setPen(QPen(Qt::NoPen)); + current->setCurveAttribute(QwtPlotCurve::Inverted, true); + + QwtSymbol *sym = new QwtSymbol; + sym->setStyle(QwtSymbol::NoSymbol); + current->setSymbol(sym); + + // fudge for date ranges, not for time of day graph + // and fudge qwt'S lack of a decent bar chart + // add a zero point at the head and tail so the + // histogram columns look nice. + // and shift all the x-values left by 0.5 so that + // they centre over x-axis labels + int i=0; + for (i=0; i xaxis (xdata.size() * 4); + QVector yaxis (ydata.size() * 4); + + // samples to time + for (int i=0, offset=0; isetSamples(xdata.data(),ydata.data(), count + 1); + current->setBaseline(metricDetail.baseline); + + // update stack data so we can index off them + // in tooltip + *stackX[stackcounter+1] = xdata; + *stackY[stackcounter+1] = ydata; + + // update min/max Y values for the chosen axis + if (current->maxYValue() > maxY[supportedAxes.indexOf(axisid)]) maxY[supportedAxes.indexOf(axisid)] = current->maxYValue(); + if (current->minYValue() < minY[supportedAxes.indexOf(axisid)]) minY[supportedAxes.indexOf(axisid)] = current->minYValue(); + + current->attach(this); + + } // end of reverse for stacked plots + + //qDebug()<<"First plotting iteration.."<metrics) { + + // + // *ONLY* PLOT NON-STACKS + // + if (metricDetail.stack == true) continue; + + QVector xdata, ydata; + + int count; + if (settings->groupBy != LTM_TOD) + createCurveData(cd.sourceContext, settings, metricDetail, xdata, ydata, count); + else + createTODCurveData(cd.sourceContext, settings, metricDetail, xdata, ydata, count); + + // lets catch the x-scale + if (count > MAXX) MAXX=count; + + //qDebug()<<"Create curve data.."<value(this, GC_ANTIALIAS, false).toBool() == true) + current->setRenderHint(QwtPlotItem::RenderAntialiased); + QPen cpen = QPen(cd.color); + cpen.setWidth(width); + current->setPen(cpen); + current->setStyle(metricDetail.curveStyle); + + // choose the axis + axisid = chooseYAxis(metricDetail.uunits); + current->setYAxis(axisid); + + // left and right offset for bars + double left = 0; + double right = 0; + double middle = 0; + if (metricDetail.curveStyle == QwtPlotCurve::Steps) { + + // we still worry about stacked bars, since we + // need to take into account the space it will + // consume when plotted in the second iteration + // below this one + int barn = cdCount; + + double space = double(0.9) / bars; + double gap = space * 0.10; + double width = space * 0.90; + left = (space * barn) + (gap / 2) + 0.1; + right = left + width; + middle = ((left+right) / double(2)) - 0.5; + } + + // trend - clone the data for the curve and add a curvefitted + if (metricDetail.trendtype) { + + // linear regress + if (metricDetail.trendtype == 1 && count > 2) { + + // override class variable as doing it temporarily for trend line only + double maxX = 0.5 + groupForDate(settings->end.date(), settings->groupBy) - + groupForDate(settings->start.date(), settings->groupBy); + + QString trendName = QString(tr("%1 trend")).arg(metricDetail.uname); + QString trendSymbol = QString("%1_trend") + .arg(metricDetail.type == METRIC_BEST ? + metricDetail.bestSymbol : metricDetail.symbol); + + QwtPlotCurve *trend = new QwtPlotCurve(""); + curves.insert("", trend); + + // cosmetics + QPen cpen = QPen(cd.color.darker(200)); + cpen.setWidth(2); // double thickness for trend lines + cpen.setStyle(Qt::SolidLine); + trend->setPen(cpen); + if (appsettings->value(this, GC_ANTIALIAS, false).toBool()==true) + trend->setRenderHint(QwtPlotItem::RenderAntialiased); + trend->setBaseline(0); + trend->setYAxis(axisid); + trend->setStyle(QwtPlotCurve::Lines); + + // perform linear regression + LTMTrend regress(xdata.data(), ydata.data(), count); + double xtrend[2], ytrend[2]; + xtrend[0] = 0.0; + ytrend[0] = regress.getYforX(0.0); + // point 2 is at far right of chart, not the last point + // since we may be forecasting... + xtrend[1] = maxX; + ytrend[1] = regress.getYforX(maxX); + trend->setSamples(xtrend,ytrend, 2); + + trend->attach(this); + curves.insert(trendSymbol, trend); + + } + + // quadratic lsm regression + if (metricDetail.trendtype == 2 && count > 3) { + QString trendName = QString(tr("%1 trend")).arg(metricDetail.uname); + QString trendSymbol = QString("%1_trend") + .arg(metricDetail.type == METRIC_BEST ? + metricDetail.bestSymbol : metricDetail.symbol); + + QwtPlotCurve *trend = new QwtPlotCurve(""); + curves.insert("", trend); + + // cosmetics + QPen cpen = QPen(cd.color.darker(200)); + cpen.setWidth(2); // double thickness for trend lines + cpen.setStyle(Qt::SolidLine); + trend->setPen(cpen); + if (appsettings->value(this, GC_ANTIALIAS, false).toBool()==true) + trend->setRenderHint(QwtPlotItem::RenderAntialiased); + trend->setBaseline(0); + trend->setYAxis(axisid); + trend->setStyle(QwtPlotCurve::Lines); + + // perform quadratic curve fit to data + LTMTrend2 regress(xdata.data(), ydata.data(), count); + + QVector xtrend; + QVector ytrend; + + double inc = (regress.maxx - regress.minx) / 100; + for (double i=regress.minx; i<=(regress.maxx+inc); i+= inc) { + xtrend << i; + ytrend << regress.yForX(i); + } + + // point 2 is at far right of chart, not the last point + // since we may be forecasting... + trend->setSamples(xtrend.data(),ytrend.data(), xtrend.count()); + + trend->attach(this); + curves.insert(trendSymbol, trend); + } + } + + // highlight outliers + if (metricDetail.topOut > 0 && metricDetail.topOut < count && count > 10) { + + LTMOutliers outliers(xdata.data(), ydata.data(), count, 10); + + // the top 5 outliers + QVector hxdata, hydata; + hxdata.resize(metricDetail.topOut); + hydata.resize(metricDetail.topOut); + + // QMap orders the list so start at the top and work + // backwards + for (int i=0; i 1) + outName = QString(tr("%1 Top %2 Outliers")) + .arg(metricDetail.uname) + .arg(metricDetail.topOut); + else + outName = QString(tr("%1 Outlier")).arg(metricDetail.uname); + + QString outSymbol = QString("%1_outlier").arg(metricDetail.type == METRIC_BEST ? + metricDetail.bestSymbol : metricDetail.symbol); + QwtPlotCurve *out = new QwtPlotCurve(""); + curves.insert("", out); + + out->setRenderHint(QwtPlotItem::RenderAntialiased); + out->setStyle(QwtPlotCurve::Dots); + + // we might have hidden the symbols for this curve + // if its set to none then default to a rectangle + QwtSymbol *sym = new QwtSymbol; + if (metricDetail.symbolStyle == QwtSymbol::NoSymbol) { + sym->setStyle(QwtSymbol::Ellipse); + sym->setSize(10); + } else { + sym->setStyle(metricDetail.symbolStyle); + sym->setSize(20); + } + QColor lighter = cd.color; + lighter.setAlpha(50); + sym->setPen(cd.color); + sym->setBrush(lighter); + + out->setSymbol(sym); + out->setSamples(hxdata.data(),hydata.data(), metricDetail.topOut); + out->setBaseline(0); + out->setYAxis(axisid); + out->attach(this); + } + + // highlight top N values + if (metricDetail.topN > 0) { + + QMap sortedList; + + // copy the yvalues, retaining the offset + for(int i=0; i hxdata, hydata; + hxdata.resize(metricDetail.topN); + hydata.resize(metricDetail.topN); + + // QMap orders the list so start at the top and work + // backwards + QMapIterator i(sortedList); + i.toBack(); + int counter = 0; + while (i.hasPrevious() && counter < metricDetail.topN) { + i.previous(); + if (ydata[i.value()]) { + hxdata[counter] = xdata[i.value()] + middle; + hydata[counter] = ydata[i.value()]; + counter++; + } + } + + // lets setup a curve with this data then! + QString topName; + if (counter > 1) + topName = QString(tr("%1 Best %2")) + .arg(metricDetail.uname) + .arg(counter); // starts from zero + else + topName = QString(tr("Best %1")).arg(metricDetail.uname); + + QString topSymbol = QString("%1_topN") + .arg(metricDetail.type == METRIC_BEST ? + metricDetail.bestSymbol : metricDetail.symbol); + QwtPlotCurve *top = new QwtPlotCurve(""); + curves.insert("", top); + + top->setRenderHint(QwtPlotItem::RenderAntialiased); + top->setStyle(QwtPlotCurve::Dots); + + // we might have hidden the symbols for this curve + // if its set to none then default to a rectangle + QwtSymbol *sym = new QwtSymbol; + if (metricDetail.symbolStyle == QwtSymbol::NoSymbol) { + sym->setStyle(QwtSymbol::Ellipse); + sym->setSize(6); + } else { + sym->setStyle(metricDetail.symbolStyle); + sym->setSize(12); + } + QColor lighter = cd.color; + lighter.setAlpha(200); + sym->setPen(cd.color); + sym->setBrush(lighter); + + top->setSymbol(sym); + top->setSamples(hxdata.data(),hydata.data(), counter); + top->setBaseline(0); + top->setYAxis(axisid); + top->attach(this); + + // if we haven't already got data labels selected for this curve + // then lets put some on, just for the topN, since they are of + // interest to the user and typically the first thing they do + // is move mouse over to get a tooltip anyway! + if (!metricDetail.labels) { + + QFont labelFont; + labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString()); + labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt()); + + // loop through each NONZERO value and add a label + for (int i=0; iprecision(); + if (metricDetail.uunits == "seconds") precision=1; + if (metricDetail.uunits == "km") precision=0; + + // we have a metric so lets be precise ... + labelString = QString("%1").arg(value * (context->athlete->useMetricUnits ? 1 : m->conversion()) + + (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision); + + } else { + // no precision + labelString = (QString("%1").arg(value, 0, 'f', 0)); + } + + + // Qwt uses its own text objects + QwtText text(labelString); + text.setFont(labelFont); + text.setColor(cd.color); + + // make that mark -- always above with topN + QwtPlotMarker *label = new QwtPlotMarker(); + label->setLabel(text); + label->setValue(hxdata[i], hydata[i]); + label->setYAxis(axisid); + label->setSpacing(6); // not px but by yaxis value !? mad. + label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter); + + // and attach + label->attach(this); + labels << label; + } + } + } + } + + if (metricDetail.curveStyle == QwtPlotCurve::Steps) { + + // fill the bars + QColor brushColor = cd.color; + brushColor.setAlpha(64); // now side by side, less transparency required + QColor brushColor1 = cd.color.darker(); + QLinearGradient linearGradient(0, 0, 0, height()); + linearGradient.setColorAt(0.0, brushColor1); + linearGradient.setColorAt(1.0, brushColor); + linearGradient.setSpread(QGradient::PadSpread); + current->setBrush(linearGradient); + current->setPen(QPen(Qt::NoPen)); + current->setCurveAttribute(QwtPlotCurve::Inverted, true); + + QwtSymbol *sym = new QwtSymbol; + sym->setStyle(QwtSymbol::NoSymbol); + current->setSymbol(sym); + + // fudge for date ranges, not for time of day graph + // fudge qwt'S lack of a decent bar chart + // add a zero point at the head and tail so the + // histogram columns look nice. + // and shift all the x-values left by 0.5 so that + // they centre over x-axis labels + count = xdata.size()-2; + + int i=0; + for (i=0; i xaxis (xdata.size() * 4); + QVector yaxis (ydata.size() * 4); + + // samples to time + for (int i=0, offset=0; isetSize(6); + sym->setStyle(metricDetail.symbolStyle); + sym->setPen(QPen(cd.color)); + sym->setBrush(QBrush(cd.color)); + current->setSymbol(sym); + current->setPen(cpen); + + // fill below the line + if (metricDetail.fillCurve) { + QColor fillColor = cd.color; + fillColor.setAlpha(60); + current->setBrush(fillColor); + } + + + } else if (metricDetail.curveStyle == QwtPlotCurve::Dots) { + + QwtSymbol *sym = new QwtSymbol; + sym->setSize(6); + sym->setStyle(metricDetail.symbolStyle); + sym->setPen(QPen(cd.color)); + sym->setBrush(QBrush(cd.color)); + current->setSymbol(sym); + + } else if (metricDetail.curveStyle == QwtPlotCurve::Sticks) { + + QwtSymbol *sym = new QwtSymbol; + sym->setSize(4); + sym->setStyle(metricDetail.symbolStyle); + sym->setPen(QPen(cd.color)); + sym->setBrush(QBrush(Qt::white)); + current->setSymbol(sym); + + } + + // add data labels + if (metricDetail.labels) { + + QFont labelFont; + labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString()); + labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt()); + + // loop through each NONZERO value and add a label + for (int i=0; iprecision(); + if (metricDetail.uunits == "seconds") precision=1; + if (metricDetail.uunits == "km") precision=0; + + // we have a metric so lets be precise ... + labelString = QString("%1").arg(value * (context->athlete->useMetricUnits ? 1 : m->conversion()) + + (context->athlete->useMetricUnits ? 0 : m->conversionSum()), 0, 'f', precision); + + } else { + // no precision + labelString = (QString("%1").arg(value, 0, 'f', 0)); + } + + + // Qwt uses its own text objects + QwtText text(labelString); + text.setFont(labelFont); + text.setColor(cd.color); + + // make that mark + QwtPlotMarker *label = new QwtPlotMarker(); + label->setLabel(text); + label->setValue(xdata[i], ydata[i]); + label->setYAxis(axisid); + label->setSpacing(3); // not px but by yaxis value !? mad. + + // Bars(steps) / sticks / dots: label above centered + // but bars have multiple points offset from their actual + // so need to adjust bars to centre above the top of the bar + if (metricDetail.curveStyle == QwtPlotCurve::Steps) { + + // We only get every fourth point, so center + // between second and third point of bar "square" + label->setValue((xdata[i-1]+xdata[i-2])/2.00f, ydata[i-1]); + } + + // Lables on a Line curve should be above/below depending upon the shape of the curve + if (metricDetail.curveStyle == QwtPlotCurve::Lines) { + + label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter); + + // we could simplify this into one if clause but it wouldn't be + // so obvious what we were doing + if (i && (i == ydata.count()-3) && ydata[i-1] > ydata[i]) { + + // last point on curve + label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter); + + } else if (i && i < ydata.count()) { + + // is a low / valley + if (ydata[i-1] > ydata[i] && ydata[i+1] > ydata[i]) + label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter); + + } else if (i == 0 && ydata[i+1] > ydata[i]) { + + // first point on curve + label->setLabelAlignment(Qt::AlignBottom | Qt::AlignCenter); + } + + } else { + + label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter); + } + + // and attach + label->attach(this); + labels << label; + } + } + } + + // smoothing + if (metricDetail.smooth == true) { + current->setCurveAttribute(QwtPlotCurve::Fitted, true); + } + + // set the data series + current->setSamples(xdata.data(),ydata.data(), count + 1); + current->setBaseline(metricDetail.baseline); + + //qDebug()<<"Set Curve Data.."<maxYValue() > maxY[supportedAxes.indexOf(axisid)]) maxY[supportedAxes.indexOf(axisid)] = current->maxYValue(); + if (current->minYValue() < minY[supportedAxes.indexOf(axisid)]) minY[supportedAxes.indexOf(axisid)] = current->minYValue(); + + current->attach(this); + + } + } + + //qDebug()<<"Second plotting iteration.."<groupBy != LTM_TOD) { + + int tics; + if (MAXX < 14) { + tics = 1; + } else { + tics = 1 + MAXX/10; + } + setAxisScale(xBottom, -0.498f, MAXX+0.498f, tics); + setAxisScaleDraw(QwtPlot::xBottom, new CompareScaleDraw()); + + + } else { + setAxisScale(xBottom, 0, 24, 2); + setAxisScaleDraw(QwtPlot::xBottom, new LTMScaleDraw(settings->start, + groupForDate(settings->start.date(), settings->groupBy), settings->groupBy)); + } + enableAxis(QwtAxis::xBottom, true); + setAxisVisible(QwtAxis::xBottom, true); + setAxisVisible(QwtAxis::xTop, false); + + // run through the Y axis + for (int i=0; i<8; i++) { + // set the scale on the axis + if (i != xBottom && i != xTop) { + maxY[i] *= 1.2; // add 20% headroom + setAxisScale(supportedAxes[i], minY[i], maxY[i]); + } + } + + // if not stacked then lets make the yAxis a little + // more descriptive and use the color of the curve + if (set->metrics.count() == 1) { + + // title (units) + QString units = set->metrics[0].uunits; + QString name = set->metrics[0].uname; + + // abbreviate the coggan bullshit everyone loves + // but god only knows why (sheep?) + if (name == "Coggan Acute Training Load") name = "ATL"; + if (name == "Coggan Chronic Training Load") name = "CTL"; + if (name == "Coggan Training Stress Balance") name = "TSB"; + + QString title = name ; + if (units != "" && units != name) title = title + " (" + units + ")"; + + setAxisTitle(axisid, title); + + // color + QPalette pal; + pal.setColor(QPalette::WindowText, set->metrics[0].penColor); + pal.setColor(QPalette::Text, set->metrics[0].penColor); + axisWidget(axisid)->setPalette(pal); + } + + QString format = axisTitle(yLeft).text(); + parent->toolTip()->setAxes(xBottom, yLeft); + parent->toolTip()->setFormat(format); + + // show legend? + if (settings->legend == false) this->legend()->hide(); + else this->legend()->show(); + + QHashIterator p(curves); + while (p.hasNext()) { + p.next(); + if (p.key() == "") // hide bollocksy curves + p.value()->setItemAttribute(QwtPlotItem::Legend, false); + else + p.value()->setItemAttribute(QwtPlotItem::Legend, settings->legend); + } + + // now refresh + updateLegend(); + + // markers + //if (settings->groupBy != LTM_TOD) + // refreshMarkers(settings->start.date(), settings->end.date(), settings->groupBy); + + //qDebug()<<"Final tidy.."<&x,QVector&y,int&n) { y.clear(); x.clear(); @@ -1176,13 +2181,14 @@ LTMPlot::createTODCurveData(LTMSettings *settings, MetricDetail metricDetail, QV } void -LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVector&x,QVector&y,int&n) +LTMPlot::createCurveData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QVector&x,QVector&y,int&n) { QList *data = NULL; // resize the curve array to maximum possible size int maxdays = groupForDate(settings->end.date(), settings->groupBy) - groupForDate(settings->start.date(), settings->groupBy); + x.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail y.resize(maxdays+3); // one for start from zero plus two for 0 value added at head and tail @@ -1194,7 +2200,7 @@ LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVect } else if (metricDetail.type == METRIC_MEASURE) { data = settings->measures; } else if (metricDetail.type == METRIC_PM) { - createPMCCurveData(settings, metricDetail, PMCdata); + createPMCCurveData(context, settings, metricDetail, PMCdata); data = &PMCdata; } else if (metricDetail.type == METRIC_BEST) { data = settings->bests; @@ -1203,7 +2209,7 @@ LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVect n=-1; int lastDay=0; unsigned long secondsPerGroupBy=0; - bool wantZero = (metricDetail.curveStyle == QwtPlotCurve::Steps); + bool wantZero = metricDetail.curveStyle == QwtPlotCurve::Steps; foreach (SummaryMetrics rideMetrics, *data) { // filter out unwanted rides but not for PMC type metrics @@ -1244,7 +2250,7 @@ LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVect if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_MEASURE) seconds = 1; if (currentDay > lastDay) { if (lastDay && wantZero) { - while (lastDaystart.date(), settings->groupBy); @@ -1266,6 +2272,9 @@ LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVect if (metricDetail.type == METRIC_BEST) type = RideMetric::Peak; + // first time thru + if (n<0) n++; + switch (type) { case RideMetric::Total: y[n] += value; @@ -1293,7 +2302,7 @@ LTMPlot::createCurveData(LTMSettings *settings, MetricDetail metricDetail, QVect } void -LTMPlot::createPMCCurveData(LTMSettings *settings, MetricDetail metricDetail, +LTMPlot::createPMCCurveData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QList &customData) { diff --git a/src/LTMPlot.h b/src/LTMPlot.h index 91e576283..a9dd08b3d 100644 --- a/src/LTMPlot.h +++ b/src/LTMPlot.h @@ -38,6 +38,7 @@ class LTMPlotBackground; class LTMWindow; class LTMPlotZoneLabel; class LTMScaleDraw; +class CompareScaleDraw; class StressCalculator; class LTMPlot : public QwtPlot @@ -50,6 +51,7 @@ class LTMPlot : public QwtPlot LTMPlot(LTMWindow *, Context *context); ~LTMPlot(); void setData(LTMSettings *); + void setCompareData(LTMSettings *); void setAxisTitle(QwtAxisId axis, QString label); public slots: @@ -68,7 +70,7 @@ class LTMPlot : public QwtPlot LTMWindow *parent; double minY[10], maxY[10], maxX; // for all possible 10 curves void resetPMC(); - void createPMCCurveData(LTMSettings *, MetricDetail, QList &); + void createPMCCurveData(Context *,LTMSettings *, MetricDetail, QList &); private: Context *context; @@ -94,10 +96,8 @@ class LTMPlot : public QwtPlot QVector< QVector* > stackY; int groupForDate(QDate , int); - void createCurveData(LTMSettings *, MetricDetail, - QVector&, QVector&, int&); - void createTODCurveData(LTMSettings *, MetricDetail, - QVector&, QVector&, int&); + void createCurveData(Context *,LTMSettings *, MetricDetail, QVector&, QVector&, int&); + void createTODCurveData(Context *,LTMSettings *, MetricDetail, QVector&, QVector&, int&); void aggregateCurves(QVector &a, QVector&w); // aggregate a with w, updates a QwtAxisId chooseYAxis(QString); void refreshZoneLabels(QwtAxisId); @@ -110,6 +110,17 @@ class LTMPlot : public QwtPlot QList supportedAxes; }; +class CompareScaleDraw: public QwtScaleDraw +{ + public: + CompareScaleDraw() {} + + virtual QwtText label(double v) const { + + return QwtText(QString("%1%2").arg(v ? "+" : "").arg(int(v))); + } +}; + // Produce Labels for X-Axis class LTMScaleDraw: public QwtScaleDraw { diff --git a/src/LTMWindow.cpp b/src/LTMWindow.cpp index 77aed9b91..ee89846b3 100644 --- a/src/LTMWindow.cpp +++ b/src/LTMWindow.cpp @@ -41,7 +41,7 @@ #include LTMWindow::LTMWindow(Context *context) : - GcChartWindow(context), context(context), dirty(true), stackDirty(true) + GcChartWindow(context), context(context), dirty(true), stackDirty(true), compareDirty(true) { useToToday = useCustom = false; plotted = DateRange(QDate(01,01,01), QDate(01,01,01)); @@ -79,12 +79,27 @@ LTMWindow::LTMWindow(Context *context) : dataSummary->settings()->setFontSize(QWebSettings::DefaultFontSize, defaultFont.pointSize()+1); dataSummary->settings()->setFontFamily(QWebSettings::StandardFont, defaultFont.family()); + // compare plot page + compareplotsWidget = new QWidget(this); + compareplotsWidget->setPalette(palette); + compareplotsLayout = new QVBoxLayout(compareplotsWidget); + compareplotsLayout->setSpacing(0); + compareplotsLayout->setContentsMargins(0,0,0,0); + + compareplotArea = new QScrollArea(this); + compareplotArea->setAutoFillBackground(false); + compareplotArea->setWidgetResizable(true); + compareplotArea->setWidget(compareplotsWidget); + compareplotArea->setFrameStyle(QFrame::NoFrame); + compareplotArea->setContentsMargins(0,0,0,0); + compareplotArea->setPalette(palette); // the stack stackWidget = new QStackedWidget(this); stackWidget->addWidget(ltmPlot); stackWidget->addWidget(dataSummary); stackWidget->addWidget(plotArea); + stackWidget->addWidget(compareplotArea); stackWidget->setCurrentIndex(0); mainLayout->addWidget(stackWidget); setChartLayout(mainLayout); @@ -184,6 +199,10 @@ LTMWindow::LTMWindow(Context *context) : connect(ltmTool, SIGNAL(curvesChanged()), this, SLOT(refresh())); connect(context, SIGNAL(filterChanged()), this, SLOT(refresh())); + // comparing things + connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged())); + connect(context, SIGNAL(compareDateRangesChanged()), this, SLOT(compareChanged())); + // connect pickers to ltmPlot connect(_canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), ltmPlot, SLOT(pointHover(QwtPlotCurve*, int))); connect(_canvasPicker, SIGNAL(pointClicked(QwtPlotCurve*, int)), ltmPlot, SLOT(pointClicked(QwtPlotCurve*, int))); @@ -198,6 +217,28 @@ LTMWindow::~LTMWindow() delete popup; } +void +LTMWindow::compareChanged() +{ + if (!amVisible()) { + compareDirty = true; + return; + } + + if (isCompare()) { + + // refresh plot handles the compare case + refreshPlot(); + + } else { + + // forced refresh back to normal + stackDirty = dirty = true; + filterChanged(); // forces reread etc + } + repaint(); +} + void LTMWindow::rideSelected() { } // deprecated @@ -206,7 +247,13 @@ LTMWindow::refreshPlot() { if (amVisible() == true) { - if (ltmTool->showData->isChecked()) { + if (isCompare()) { + + // COMPARE PLOTS + stackWidget->setCurrentIndex(3); + refreshCompare(); + + } else if (ltmTool->showData->isChecked()) { // DATA TABLE stackWidget->setCurrentIndex(1); @@ -234,9 +281,101 @@ LTMWindow::refreshPlot() } } +void +LTMWindow::refreshCompare() +{ + // not if in compare mode + if (!isCompare()) return; + + // setup stacks but only if needed + //if (!stackDirty) return; // lets come back to that! + + setUpdatesEnabled(false); + + // delete old and create new... + // QScrollArea *plotArea; + // QWidget *plotsWidget; + // QVBoxLayout *plotsLayout; + // QList plotSettings; + foreach (LTMPlot *p, compareplots) { + compareplotsLayout->removeWidget(p); + delete p; + } + compareplots.clear(); + compareplotSettings.clear(); + + if (compareplotsLayout->count() == 1) { + compareplotsLayout->takeAt(0); // remove the stretch + } + + // now lets create them all again + // based upon the current setttings + // we create a plot for each curve + // but where they are stacked we put + // them all in the SAME plot + // so we go once through picking out + // the stacked items and once through + // for all the rest of the curves + LTMSettings plotSetting = settings; + plotSetting.metrics.clear(); + foreach(MetricDetail m, settings.metrics) { + if (m.stack) plotSetting.metrics << m; + } + + // create ltmPlot with this + if (plotSetting.metrics.count()) { + + compareplotSettings << plotSetting; + + // create and setup the plot + LTMPlot *stacked = new LTMPlot(this, context); + stacked->setCompareData(&compareplotSettings.last()); // setData using the compare data + stacked->setFixedHeight(200); // maybe make this adjustable later + + // now add + compareplotsLayout->addWidget(stacked); + compareplots << stacked; + } + + // OK, now one plot for each curve + // that isn't stacked! + foreach(MetricDetail m, settings.metrics) { + + // ignore stacks + if (m.stack) continue; + + plotSetting = settings; + plotSetting.metrics.clear(); + plotSetting.metrics << m; + compareplotSettings << plotSetting; + + // create and setup the plot + LTMPlot *plot = new LTMPlot(this, context); + plot->setCompareData(&compareplotSettings.last()); // setData using the compare data + + // now add + compareplotsLayout->addWidget(plot); + compareplots << plot; + } + + // squash em up + compareplotsLayout->addStretch(); + + // resize to choice + zoomSliderChanged(); + + // we no longer dirty + compareDirty = false; + + setUpdatesEnabled(true); +} + void LTMWindow::refreshStackPlots() { + // not if in compare mode + if (isCompare()) return; + // setup stacks but only if needed //if (!stackDirty) return; // lets come back to that! @@ -328,9 +467,16 @@ LTMWindow::zoomSliderChanged() settings.stackWidth = ltmTool->stackSlider->value(); setUpdatesEnabled(false); + + // do the compare and the noncompare plots + // at the same time, as we don't need to worry + // about optimising out as its fast anyway foreach(LTMPlot *plot, plots) { plot->setFixedHeight(150 + add[index]); } + foreach(LTMPlot *plot, compareplots) { + plot->setFixedHeight(150 + add[index]); + } setUpdatesEnabled(true); } @@ -366,6 +512,8 @@ LTMWindow::useThruToday() void LTMWindow::refresh() { + // not if in compare mode + if (isCompare()) return; // refresh for changes to ridefiles / zones if (amVisible() == true && context->athlete->metricDB != NULL) { @@ -396,14 +544,30 @@ LTMWindow::dateRangeChanged(DateRange range) settings.measures = &measures; settings.bests = &bestsresults; - // apply filter to new date range too -- will also refresh plot - filterChanged(); + // we let all the state get updated, but lets not actually plot + // whilst in compare mode -- but when compare mode ends we will + // call filterChanged, so need to record the fact that the date + // range changed whilst we were in compare mode + if (!isCompare()) { + + // apply filter to new date range too -- will also refresh plot + filterChanged(); + } else { + + // we've been told to redraw so maybe + // compare mode was switched whilst we were + // not visible, lets refresh + if (compareDirty) compareChanged(); + } } } void LTMWindow::filterChanged() { + // ignore in compare mode + if (isCompare()) return; + if (amVisible() == false || context->athlete->metricDB == NULL) return; if (useCustom) { @@ -720,7 +884,7 @@ LTMWindow::refreshDataTable() data = settings.measures; } else if (metricDetail.type == METRIC_PM) { // PMC fixup later - ltmPlot->createPMCCurveData(&settings, metricDetail, PMCdata); + ltmPlot->createPMCCurveData(context, &settings, metricDetail, PMCdata); data = &PMCdata; } else if (metricDetail.type == METRIC_BEST) { data = settings.bests; diff --git a/src/LTMWindow.h b/src/LTMWindow.h index c69873c00..a93d87968 100644 --- a/src/LTMWindow.h +++ b/src/LTMWindow.h @@ -119,6 +119,9 @@ class LTMWindow : public GcChartWindow bool isFiltered() const { return (ltmTool->isFiltered() || context->ishomefiltered || context->isfiltered); } #endif + // comparing things + bool isCompare() const { return context->isCompareDateRanges; } + // used by children Context *context; @@ -179,9 +182,11 @@ class LTMWindow : public GcChartWindow void rideSelected(); // notification to refresh void refreshPlot(); // normal mode + void refreshCompare(); // compare mode void refreshStackPlots(); // stacked plots void refreshDataTable(); // data table + void compareChanged(); void dateRangeChanged(DateRange); void filterChanged(); void groupBySelected(int); @@ -227,6 +232,7 @@ class LTMWindow : public GcChartWindow // local state bool dirty; bool stackDirty; + bool compareDirty; LTMSettings settings; // all the plot settings QList results; @@ -240,6 +246,14 @@ class LTMWindow : public GcChartWindow QList plotSettings; QList plots; + // when comparing things we have a plot for each data series + // with a curve for each date range on the plot + QScrollArea *compareplotArea; + QWidget *compareplotsWidget; + QVBoxLayout *compareplotsLayout; + QList compareplotSettings; + QList compareplots; + // Widgets LTMPlot *ltmPlot; LTMTool *ltmTool; diff --git a/src/PeakPower.cpp b/src/PeakPower.cpp index 2e37ec376..3972a0683 100644 --- a/src/PeakPower.cpp +++ b/src/PeakPower.cpp @@ -92,10 +92,10 @@ class PeakPower : public RideMetric { RideMetric *clone() const { return new PeakPower(*this); } }; -class CriticalPower : public PeakPower { - Q_DECLARE_TR_FUNCTIONS(CriticalPower) +class PeakPower60m : public PeakPower { + Q_DECLARE_TR_FUNCTIONS(PeakPower60m) public: - CriticalPower() + PeakPower60m() { setSecs(3600); setSymbol("60m_critical_power"); @@ -106,7 +106,7 @@ class CriticalPower : public PeakPower { setMetricUnits(tr("watts")); setImperialUnits(tr("watts")); } - RideMetric *clone() const { return new CriticalPower(*this); } + RideMetric *clone() const { return new PeakPower60m(*this); } }; class PeakPower1s : public PeakPower { @@ -530,7 +530,7 @@ static bool addAllPeaks() { RideMetricFactory::instance().addMetric(PeakPower10m()); RideMetricFactory::instance().addMetric(PeakPower20m()); RideMetricFactory::instance().addMetric(PeakPower30m()); - RideMetricFactory::instance().addMetric(CriticalPower()); + RideMetricFactory::instance().addMetric(PeakPower60m()); RideMetricFactory::instance().addMetric(PeakPower90m()); RideMetricFactory::instance().addMetric(PeakPowerHr1m()); RideMetricFactory::instance().addMetric(PeakPowerHr5m());