/* * Copyright (c) 2009 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 "ErgFilePlot.h" #include "WPrime.h" #include "Context.h" // Bridge between QwtPlot and ErgFile to avoid having to // create a separate array for the ergfile data, we plot // directly from the ErgFile points array double ErgFileData::x(size_t i) const { if (context->currentErgFile()) return context->currentErgFile()->Points.at(i).x; else return 0; } double ErgFileData::y(size_t i) const { if (context->currentErgFile()) return context->currentErgFile()->Points.at(i).y; else return 0; } size_t ErgFileData::size() const { if (context->currentErgFile()) return context->currentErgFile()->Points.count(); else return 0; } QPointF ErgFileData::sample(size_t i) const { return QPointF(x(i), y(i)); } QRectF ErgFileData::boundingRect() const { if (context->currentErgFile()) { double minX, minY, maxX, maxY; minX=minY=maxX=maxY=0.0f; foreach(ErgFilePoint x, context->currentErgFile()->Points) { if (x.y > maxY) maxY = x.y; if (x.x > maxX) maxX = x.x; if (x.y < minY) minY = x.y; if (x.x < minX) minX = x.x; } maxY *= 1.3f; // always need a bit of headroom return QRectF(minX, minY, maxX, maxY); } return QRectF(0,0,0,0); } // Now bar double NowData::x(size_t) const { return context->getNow(); } double NowData::y(size_t i) const { if (i) { if (context->currentErgFile()) return context->currentErgFile()->maxY; else return 0; } else return 0; } size_t NowData::size() const { return 2; } QPointF NowData::sample(size_t i) const { return QPointF(x(i), y(i)); } /*QRectF NowData::boundingRect() const { // TODO dgr return QRectF(0, 0, 0, 0); }*/ ErgFilePlot::ErgFilePlot(Context *context) : context(context) { //insertLegend(new QwtLegend(), QwtPlot::BottomLegend); setCanvasBackground(GColor(CTRAINPLOTBACKGROUND)); 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"); enableAxis(yLeft, true); QwtScaleDraw *sd = new QwtScaleDraw; sd->setTickLength(QwtScaleDiv::MajorTick, 3); setAxisMaxMinor(yLeft, 0); //setAxisScaleDraw(QwtPlot::yLeft, sd); QPalette pal; pal.setColor(QPalette::WindowText, GColor(CRIDEPLOTYAXIS)); pal.setColor(QPalette::Text, GColor(CRIDEPLOTYAXIS)); axisWidget(QwtPlot::yLeft)->setPalette(pal); QFont stGiles; stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString()); stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt()); QwtText title("Watts"); title.setFont(stGiles); QwtPlot::setAxisFont(yLeft, stGiles); QwtPlot::setAxisTitle(yLeft, title); enableAxis(xBottom, true); distdraw = new DistScaleDraw; distdraw->setTickLength(QwtScaleDiv::MajorTick, 3); timedraw = new HourTimeScaleDraw; timedraw->setTickLength(QwtScaleDiv::MajorTick, 3); setAxisMaxMinor(xBottom, 0); setAxisScaleDraw(QwtPlot::xBottom, timedraw); // set the axis so we default to an hour workout setAxisScale(xBottom, (double)0, 1000 * 60 * 60 , 15 * 60 * 1000); title.setFont(stGiles); title.setText("Time (mins)"); QwtPlot::setAxisFont(xBottom, stGiles); QwtPlot::setAxisTitle(xBottom, title); pal.setColor(QPalette::WindowText, GColor(CRIDEPLOTXAXIS)); pal.setColor(QPalette::Text, GColor(CRIDEPLOTXAXIS)); axisWidget(QwtPlot::xBottom)->setPalette(pal); // axis 1 not currently used setAxisVisible(QwtAxisId(QwtAxis::yRight,1), false); // max speed of 60mph/60kmh seems ok to me! enableAxis(QwtAxisId(QwtAxis::yRight,1).id, false); // set all the orher axes off but scaled setAxisScale(yLeft, 0, 300); // max cadence and hr enableAxis(yLeft, true); setAxisAutoScale(QwtPlot::yLeft, true);// we autoscale, since peaks are so much higher than troughs setAxisScale(yRight, 0, 250); // max cadence and hr enableAxis(yRight, false); setAxisScale(QwtAxisId(QwtAxis::yRight,2), 0, 60); // max speed of 60mph/60kmh seems ok to me! setAxisVisible(QwtAxisId(QwtAxis::yRight,2), false); // max speed of 60mph/60kmh seems ok to me! enableAxis(QwtAxisId(QwtAxis::yRight,2).id, false); // data bridge to ergfile lodData = new ErgFileData(context); // Load Curve LodCurve = new QwtPlotCurve("Course Load"); LodCurve->setSamples(lodData); LodCurve->attach(this); LodCurve->setBaseline(-1000); LodCurve->setYAxis(QwtPlot::yLeft); // load curve is blue for time and grey for gradient QColor brush_color = QColor(Qt::blue); brush_color.setAlpha(64); LodCurve->setBrush(brush_color); // fill below the line 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); wbalCurve = new QwtPlotCurve("W'bal Actual"); wbalCurve->attach(this); wbalCurve->setYAxis(QwtAxisId(QwtAxis::yRight, 3)); QPen wbalPenA = QPen(GColor(CWBAL), 1.0); // actual lighter wbalCurve->setPen(wbalPenA); wbalData = new CurveData; wbalCurve->setSamples(wbalData->x(), wbalData->y(), wbalData->count()); 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); QwtPlot::setAxisFont(QwtAxisId(QwtAxis::yRight, 3), stGiles); QwtText title2("W'bal"); title2.setFont(stGiles); QwtPlot::setAxisTitle(QwtAxisId(QwtAxis::yRight,3), title2); // telemetry history wattsCurve = new QwtPlotCurve("Power"); QPen wattspen = QPen(GColor(CPOWER)); wattsCurve->setPen(wattspen); wattsCurve->attach(this); wattsCurve->setYAxis(QwtPlot::yLeft); // dgr wattsCurve->setPaintAttribute(QwtPlotCurve::PaintFiltered); wattsData = new CurveData; wattsCurve->setSamples(wattsData->x(), wattsData->y(), wattsData->count()); // telemetry history hrCurve = new QwtPlotCurve("Heartrate"); QPen hrpen = QPen(GColor(CHEARTRATE)); hrCurve->setPen(hrpen); hrCurve->attach(this); hrCurve->setYAxis(QwtPlot::yRight); hrData = new CurveData; hrCurve->setSamples(hrData->x(), hrData->y(), hrData->count()); // telemetry history cadCurve = new QwtPlotCurve("Cadence"); QPen cadpen = QPen(GColor(CCADENCE)); cadCurve->setPen(cadpen); cadCurve->attach(this); cadCurve->setYAxis(QwtPlot::yRight); cadData = new CurveData; cadCurve->setSamples(cadData->x(), cadData->y(), cadData->count()); // telemetry history speedCurve = new QwtPlotCurve("Speed"); QPen speedpen = QPen(GColor(CSPEED)); speedCurve->setPen(speedpen); speedCurve->attach(this); speedCurve->setYAxis(QwtAxisId(QwtAxis::yRight,2).id); speedData = new CurveData; speedCurve->setSamples(speedData->x(), speedData->y(), speedData->count()); // Now data bridge nowData = new NowData(context); // Now pointer NowCurve = new QwtPlotCurve("Now"); QPen Nowpen = QPen(Qt::red, 2.0); NowCurve->setPen(Nowpen); NowCurve->setSamples(nowData); NowCurve->attach(this); NowCurve->setYAxis(QwtPlot::yLeft); bydist = false; ergFile = NULL; setAutoReplot(false); setData(ergFile); connect(context, SIGNAL(configChanged()), this, SLOT(configChanged())); } void ErgFilePlot::configChanged() { setCanvasBackground(GColor(CTRAINPLOTBACKGROUND)); replot(); } void ErgFilePlot::setData(ErgFile *ergfile) { reset(); ergFile = ergfile; // clear the previous marks (if any) for(int i=0; idetach(); delete Marks.at(i); } Marks.clear(); // axis fonts QFont stGiles; stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString()); stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt()); QPalette pal; if (ergfile) { // is this by distance or time? bydist = (ergfile->format == CRS) ? true : false; if (bydist == true) { QColor brush_color1 = QColor(Qt::gray); brush_color1.setAlpha(200); QColor brush_color2 = QColor(Qt::gray); brush_color2.setAlpha(64); QLinearGradient linearGradient(0, 0, 0, height()); linearGradient.setColorAt(0.0, brush_color1); linearGradient.setColorAt(1.0, brush_color2); linearGradient.setSpread(QGradient::PadSpread); LodCurve->setBrush(linearGradient); // fill below the line QPen Lodpen = QPen(Qt::gray, 1.0); LodCurve->setPen(Lodpen); } else { QColor brush_color1 = QColor(Qt::blue); brush_color1.setAlpha(200); QColor brush_color2 = QColor(Qt::blue); brush_color2.setAlpha(64); QLinearGradient linearGradient(0, 0, 0, height()); linearGradient.setColorAt(0.0, brush_color1); linearGradient.setColorAt(1.0, brush_color2); linearGradient.setSpread(QGradient::PadSpread); LodCurve->setBrush(linearGradient); // fill below the line QPen Lodpen = QPen(Qt::blue, 1.0); LodCurve->setPen(Lodpen); } // set up again for(int i=0; i < ergFile->Laps.count(); i++) { // Show Lap Number QwtText text(ergFile->Laps.at(i).name != "" ? ergFile->Laps.at(i).name : QString::number(ergFile->Laps.at(i).LapNum)); text.setFont(QFont("Helvetica", 10, QFont::Bold)); text.setColor(GColor(CPLOTMARKER)); // vertical line QwtPlotMarker *add = new QwtPlotMarker(); add->setLineStyle(QwtPlotMarker::VLine); add->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine)); add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop); add->setValue(ergFile->Laps.at(i).x, 0.0); add->setLabel(text); add->attach(this); Marks.append(add); } // set the axis so we use all the screen estate if (context->currentErgFile() && context->currentErgFile()->Points.count()) { double maxX = (double)context->currentErgFile()->Points.last().x; if (bydist) { // tics every 5 kilometer, if workout shorter tics every 1000m double step = 5000; if (maxX <= 1000) step = 100; else if (maxX < 5000) step = 1000; // axis setup for distance setAxisScale(xBottom, (double)0, maxX, step); QwtText title; title.setFont(stGiles); title.setText("Distance (km)"); QwtPlot::setAxisFont(xBottom, stGiles); QwtPlot::setAxisTitle(xBottom, title); pal.setColor(QPalette::WindowText, Qt::gray); pal.setColor(QPalette::Text, Qt::gray); axisWidget(QwtPlot::xBottom)->setPalette(pal); // only allocate a new one if its not the current (they get freed by Qwt) if (axisScaleDraw(xBottom) != distdraw) setAxisScaleDraw(QwtPlot::xBottom, (distdraw=new DistScaleDraw())); } else { // tics every 15 minutes, if workout shorter tics every minute setAxisScale(xBottom, (double)0, maxX, maxX > (15*60*1000) ? 15*60*1000 : 60*1000); QwtText title; title.setFont(stGiles); title.setText("Time (mins)"); QwtPlot::setAxisFont(xBottom, stGiles); QwtPlot::setAxisTitle(xBottom, title); pal.setColor(QPalette::WindowText, GColor(CRIDEPLOTXAXIS)); pal.setColor(QPalette::Text, GColor(CRIDEPLOTXAXIS)); axisWidget(QwtPlot::xBottom)->setPalette(pal); // only allocate a new one if its not the current (they get freed by Qwt) if (axisScaleDraw(xBottom) != timedraw) setAxisScaleDraw(QwtPlot::xBottom, (timedraw=new HourTimeScaleDraw())); } } // compute wbal curve for the erg file calculator.setErg(ergfile); setAxisTitle(QwtAxisId(QwtAxis::yRight, 3), tr("W' Balance (j)")); setAxisScale(QwtAxisId(QwtAxis::yRight, 3),qMin(double(calculator.minY-1000),double(0)),calculator.maxY+1000); setAxisLabelAlignment(QwtAxisId(QwtAxis::yRight, 3),Qt::AlignVCenter); // and the values ... but avoid sharing! wbalCurvePredict->setSamples(calculator.xdata(false), calculator.ydata()); } else { // clear the plot we have nothing selected bydist = false; // do by time when no workout selected QwtText title; title.setFont(stGiles); title.setText("Time (mins)"); QwtPlot::setAxisFont(xBottom, stGiles); QwtPlot::setAxisTitle(xBottom, title); pal.setColor(QPalette::WindowText, GColor(CRIDEPLOTXAXIS)); pal.setColor(QPalette::Text, GColor(CRIDEPLOTXAXIS)); axisWidget(QwtPlot::xBottom)->setPalette(pal); // set the axis so we default to an hour workout if (axisScaleDraw(xBottom) != timedraw) setAxisScaleDraw(QwtPlot::xBottom, (timedraw=new HourTimeScaleDraw())); setAxisScale(xBottom, (double)0, 1000 * 60 * 60, 15*60*1000); } // make the xBottom scale visible enableAxis(xBottom, true); setAxisVisible(xBottom, true); } void ErgFilePlot::setNow(long /*msecs*/) { replot(); // and update } void ErgFilePlot::performancePlot(RealtimeData rtdata) { // we got some data // x is plotted in meters or micro-seconds double x = bydist ? (rtdata.getDistance() * 1000) : rtdata.getMsecs(); // when not using a workout we need to extend the axis when we // go out of bounds -- we do not use autoscale for x, because we // want to control stepping and tick marking add another 30 mins if (!ergFile && axisScaleDiv(QwtPlot::xBottom).upperBound() <= x) { double maxX = x + ( 30 * 60 * 1000); setAxisScale(xBottom, (double)0, maxX, maxX > (15*60*1000) ? 15*60*1000 : 60*1000); } double watts = rtdata.getWatts(); double speed = rtdata.getSpeed(); double cad = rtdata.getCadence(); double hr = rtdata.getHr(); double wbal = rtdata.getWbal(); wattssum += watts; hrsum += hr; cadsum += cad; speedsum += speed; wbalsum += wbal; if (counter < 25) { counter++; return; } else { watts = wattssum / 26; hr = hrsum / 26; cad = cadsum / 26; speed = speedsum / 26; wbal = wbalsum / 26; counter=0; wbalsum = wattssum = hrsum = cadsum = speedsum = 0; } double zero = 0; if (!wattsData->count()) wattsData->append(&zero, &watts, 1); wattsData->append(&x, &watts, 1); wattsCurve->setSamples(wattsData->x(), wattsData->y(), wattsData->count()); if (!hrData->count()) hrData->append(&zero, &hr, 1); hrData->append(&x, &hr, 1); hrCurve->setSamples(hrData->x(), hrData->y(), hrData->count()); if (!speedData->count()) speedData->append(&zero, &speed, 1); speedData->append(&x, &speed, 1); speedCurve->setSamples(speedData->x(), speedData->y(), speedData->count()); if (!cadData->count()) cadData->append(&zero, &cad, 1); cadData->append(&x, &cad, 1); cadCurve->setSamples(cadData->x(), cadData->y(), cadData->count()); if (!wbalData->count()) wbalData->append(&zero, &wbal, 1); wbalData->append(&x, &wbal, 1); wbalCurve->setSamples(wbalData->x(), wbalData->y(), wbalData->count()); } void ErgFilePlot::start() { reset(); } void ErgFilePlot::reset() { // reset data counter = wbalsum = hrsum = wattssum = speedsum = cadsum = 0; // note the origin of the data is not a point 0, but the first // average over 5 seconds. this leads to a small gap on the left // which is better than the traces all starting from 0,0 which whilst // is factually correct, does not tell us anything useful and look horrid. // instead when we place the first points on the plots we add them twice // once for time/distance of 0 and once for the current point in time wattsData->clear(); wattsCurve->setSamples(wattsData->x(), wattsData->y(), wattsData->count()); wbalData->clear(); wbalCurve->setSamples(wbalData->x(), wbalData->y(), wbalData->count()); wbalData->clear(); cadCurve->setSamples(cadData->x(), cadData->y(), cadData->count()); hrData->clear(); hrCurve->setSamples(hrData->x(), hrData->y(), hrData->count()); speedData->clear(); speedCurve->setSamples(speedData->x(), speedData->y(), speedData->count()); } // curve data.. code snaffled in from the Qwt example (realtime_plot) CurveData::CurveData(): d_count(0) { } void CurveData::append(double *x, double *y, int count) { int newSize = ((d_count + count) / 1000 + 1 ) * 1000; if (newSize > size()) { d_x.resize(newSize); d_y.resize(newSize); } for (register int i = 0; i < count; i++) { d_x[d_count + i] = x[i]; d_y[d_count + i] = y[i]; } d_count += count; } int CurveData::count() const { return d_count; } int CurveData::size() const { return d_x.size(); } const double *CurveData::x() const { return d_x.data(); } const double *CurveData::y() const { return d_y.data(); } void CurveData::clear() { d_count = 0; d_x.clear(); d_y.clear(); }