Files
GoldenCheetah/src/ErgFilePlot.cpp
Mark Liversedge 432e6ec3a9 Plot Predicted W'bal in Train mode
.. only plots predicted W'bal for the workout but at least that
   means you can see if the workout is even possible.
2014-01-28 18:32:26 +00:00

541 lines
18 KiB
C++

/*
* 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(CRIDEPLOTBACKGROUND));
static_cast<QwtPlotCanvas*>(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);
// 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).id, 0, 60); // 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);
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));
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);
}
void
ErgFilePlot::setData(ErgFile *ergfile)
{
reset();
ergFile = ergfile;
// clear the previous marks (if any)
for(int i=0; i<Marks.count(); i++) {
Marks.at(i)->detach();
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()));
}
}
// wbal predict curve and clear actual curve
QVector<double> 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
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);
}
}
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();
wattssum += watts;
hrsum += hr;
cadsum += cad;
speedsum += speed;
if (counter < 25) {
counter++;
return;
} else {
watts = wattssum / 26;
hr = hrsum / 26;
cad = cadsum / 26;
speed = speedsum / 26;
counter=0;
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());
}
void
ErgFilePlot::start()
{
reset();
}
void
ErgFilePlot::reset()
{
// reset data
counter = 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());
cadData->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();
}