mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-15 08:59:55 +00:00
The original ErgFilePlot code used local static variables to bridge between the selected ErgFile and the Qwt data handlers. This was not an issue when we only allowed one chart of this type to be available at once. But now, with a more configurable layout it is possible to have multiple. The currently selected ErgFile can now be referenced from MainWindow in the same manner as the current RideFile can. This patch introduces no functional change for users.
470 lines
15 KiB
C++
470 lines
15 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;
|
|
}
|
|
|
|
QwtData *ErgFileData::copy() const { return new ErgFileData(main); }
|
|
|
|
// 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 500;
|
|
} else return 0;
|
|
}
|
|
size_t NowData::size() const { return 2; }
|
|
QwtData *NowData::copy() const { return new NowData(main); }
|
|
|
|
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(CPOWER));
|
|
pal.setColor(QPalette::Text, GColor(CPOWER));
|
|
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);
|
|
|
|
|
|
//setAxisScale(yLeft, 0, 1000); // we autoscale, since peaks are so much higher than troughs
|
|
enableAxis(xBottom, true);
|
|
distdraw = new DistScaleDraw;
|
|
distdraw->setTickLength(QwtScaleDiv::MajorTick, 3);
|
|
timedraw = new TimeScaleDraw;
|
|
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, Qt::blue);
|
|
pal.setColor(QPalette::Text, Qt::blue);
|
|
axisWidget(QwtPlot::xBottom)->setPalette(pal);
|
|
|
|
|
|
// set all the orher axes off but scaled
|
|
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->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);
|
|
wattsCurve->setPaintAttribute(QwtPlotCurve::PaintFiltered);
|
|
wattsData = new CurveData;
|
|
wattsCurve->setRawData(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->setRawData(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->setRawData(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->setRawData(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;
|
|
|
|
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_color = QColor(Qt::gray);
|
|
brush_color.setAlpha(64);
|
|
LodCurve->setBrush(brush_color); // fill below the line
|
|
QPen Lodpen = QPen(Qt::gray, 1.0);
|
|
LodCurve->setPen(Lodpen);
|
|
|
|
} else {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
// set up again
|
|
for(int i=0; i < ergFile->Laps.count(); i++) {
|
|
|
|
// Show Lap Number
|
|
QwtText text(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, Qt::blue);
|
|
pal.setColor(QPalette::Text, Qt::blue);
|
|
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 TimeScaleDraw()));
|
|
}
|
|
}
|
|
|
|
} 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, Qt::blue);
|
|
pal.setColor(QPalette::Text, Qt::blue);
|
|
axisWidget(QwtPlot::xBottom)->setPalette(pal);
|
|
|
|
// set the axis so we default to an hour workout
|
|
if (axisScaleDraw(xBottom) != timedraw)
|
|
setAxisScaleDraw(QwtPlot::xBottom, (timedraw=new TimeScaleDraw()));
|
|
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->setRawData(wattsData->x(), wattsData->y(), wattsData->count());
|
|
|
|
if (!hrData->count()) hrData->append(&zero, &hr, 1);
|
|
hrData->append(&x, &hr, 1);
|
|
hrCurve->setRawData(hrData->x(), hrData->y(), hrData->count());
|
|
|
|
if (!speedData->count()) speedData->append(&zero, &speed, 1);
|
|
speedData->append(&x, &speed, 1);
|
|
speedCurve->setRawData(speedData->x(), speedData->y(), speedData->count());
|
|
|
|
if (!cadData->count()) cadData->append(&zero, &cad, 1);
|
|
cadData->append(&x, &cad, 1);
|
|
cadCurve->setRawData(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->setRawData(wattsData->x(), wattsData->y(), wattsData->count());
|
|
cadData->clear();
|
|
cadCurve->setRawData(cadData->x(), cadData->y(), cadData->count());
|
|
hrData->clear();
|
|
hrCurve->setRawData(hrData->x(), hrData->y(), hrData->count());
|
|
speedData->clear();
|
|
speedCurve->setRawData(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();
|
|
}
|