mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
522 lines
17 KiB
C++
522 lines
17 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"
|
|
|
|
// 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 (main->currentErgFile()) return main->currentErgFile()->Points.at(i).x;
|
|
else return 0;
|
|
}
|
|
|
|
double ErgFileData::y(size_t i) const {
|
|
if (main->currentErgFile()) return main->currentErgFile()->Points.at(i).y;
|
|
else return 0;
|
|
}
|
|
|
|
size_t ErgFileData::size() const {
|
|
if (main->currentErgFile()) return main->currentErgFile()->Points.count();
|
|
else return 0;
|
|
}
|
|
|
|
QPointF ErgFileData::sample(size_t i) const
|
|
{
|
|
return QPointF(x(i), y(i));
|
|
}
|
|
|
|
QRectF ErgFileData::boundingRect() const
|
|
{
|
|
if (main->currentErgFile()) {
|
|
double minX, minY, maxX, maxY;
|
|
minX=minY=maxX=maxY=0.0f;
|
|
foreach(ErgFilePoint x, main->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 main->getNow(); }
|
|
double NowData::y(size_t i) const {
|
|
if (i) {
|
|
if (main->currentErgFile()) return main->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(MainWindow *main) : main(main)
|
|
{
|
|
setInstanceName("ErgFile Plot");
|
|
|
|
//insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
|
|
setCanvasBackground(GColor(CRIDEPLOTBACKGROUND));
|
|
canvas()->setFrameStyle(QFrame::NoFrame);
|
|
//courseData = data; // what we plot
|
|
|
|
// 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(yRight2, 0, 60); // max speed of 60mph/60kmh seems ok to me!
|
|
enableAxis(yRight2, false);
|
|
|
|
// data bridge to ergfile
|
|
lodData = new ErgFileData(main);
|
|
// Load Curve
|
|
LodCurve = new QwtPlotCurve("Course Load");
|
|
LodCurve->setData(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);
|
|
|
|
// 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->setData(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->setData(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->setData(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(QwtPlot::yRight2);
|
|
speedData = new CurveData;
|
|
speedCurve->setData(speedData->x(), speedData->y(), speedData->count());
|
|
|
|
// Now data bridge
|
|
nowData = new NowData(main);
|
|
|
|
// Now pointer
|
|
NowCurve = new QwtPlotCurve("Now");
|
|
QPen Nowpen = QPen(Qt::red, 2.0);
|
|
NowCurve->setPen(Nowpen);
|
|
NowCurve->setData(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 (main->currentErgFile() && main->currentErgFile()->Points.count()) {
|
|
double maxX = (double)main->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()));
|
|
}
|
|
}
|
|
|
|
} 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(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->setData(wattsData->x(), wattsData->y(), wattsData->count());
|
|
|
|
if (!hrData->count()) hrData->append(&zero, &hr, 1);
|
|
hrData->append(&x, &hr, 1);
|
|
hrCurve->setData(hrData->x(), hrData->y(), hrData->count());
|
|
|
|
if (!speedData->count()) speedData->append(&zero, &speed, 1);
|
|
speedData->append(&x, &speed, 1);
|
|
speedCurve->setData(speedData->x(), speedData->y(), speedData->count());
|
|
|
|
if (!cadData->count()) cadData->append(&zero, &cad, 1);
|
|
cadData->append(&x, &cad, 1);
|
|
cadCurve->setData(cadData->x(), cadData->y(), cadData->count());
|
|
|
|
//const bool cacheMode = canvas()->testPaintAttribute(QwtPlotCanvas::PaintCached);
|
|
|
|
#if 0
|
|
#if QT_VERSION >= 0x040000 && defined(Q_WS_X11)
|
|
// Even if not recommended by TrollTech, Qt::WA_PaintOutsidePaintEvent
|
|
// works on X11. This has an tremendous effect on the performance..
|
|
|
|
canvas()->setAttribute(Qt::WA_PaintOutsidePaintEvent, true);
|
|
#endif
|
|
|
|
canvas()->setPaintAttribute(QwtPlotCanvas::PaintCached, false);
|
|
wattsCurve->draw(0, wattsCurve->dataSize() - 1);
|
|
hrCurve->draw(0, hrCurve->dataSize() - 1);
|
|
cadCurve->draw(0, cadCurve->dataSize() - 1);
|
|
speedCurve->draw(0, speedCurve->dataSize() - 1);
|
|
canvas()->setPaintAttribute(QwtPlotCanvas::PaintCached, cacheMode);
|
|
|
|
#if QT_VERSION >= 0x040000 && defined(Q_WS_X11)
|
|
canvas()->setAttribute(Qt::WA_PaintOutsidePaintEvent, false);
|
|
#endif
|
|
#endif
|
|
|
|
}
|
|
|
|
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->setData(wattsData->x(), wattsData->y(), wattsData->count());
|
|
cadData->clear();
|
|
cadCurve->setData(cadData->x(), cadData->y(), cadData->count());
|
|
hrData->clear();
|
|
hrCurve->setData(hrData->x(), hrData->y(), hrData->count());
|
|
speedData->clear();
|
|
speedCurve->setData(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();
|
|
}
|