mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
Train View: Coloring by powerzones in ErgFilePlot (#4479)
Added support to color sections according to their power zone * Optional coloring: Never (default), Always, Workout is stopped * Optional tooltip giving information about current section (independent of coloring): Never (default), Workout is stopped * Single sections covering multiple zones are split (for coloring / tooltip only) * Tooltip shows starttime, duration, power (range if applicable), zone, W'bal-range
This commit is contained in:
committed by
GitHub
parent
02835e3eb5
commit
0b144cc57b
@@ -1175,6 +1175,64 @@ ErgFile::Sections()
|
||||
return returning;
|
||||
}
|
||||
|
||||
|
||||
// convert points to set of sections, every section will be strictly in one zone
|
||||
QList<ErgFileZoneSection>
|
||||
ErgFile::ZoneSections()
|
||||
{
|
||||
QList<ErgFileZoneSection> ret;
|
||||
|
||||
const Zones *zones = context->athlete->zones("Bike");
|
||||
int zoneRange = zones->whichRange(QDate::currentDate());
|
||||
QList<QString> zoneNames = zones->getZoneNames(zoneRange);
|
||||
if (hasWatts() && Duration > 0) {
|
||||
for (int i = 0; i < Points.size() - 1; ++i) {
|
||||
const ErgFilePoint &pl = Points[i];
|
||||
const ErgFilePoint &pr = Points[i + 1];
|
||||
if (long(pl.x) != long(pr.x)) {
|
||||
int zoneL = 0;
|
||||
int zoneR = 0;
|
||||
if (zoneRange >= 0) {
|
||||
zoneL = zones->whichZone(zoneRange, pl.y);
|
||||
zoneR = zones->whichZone(zoneRange, pr.y);
|
||||
}
|
||||
if (zoneL == zoneR) {
|
||||
ret << ErgFileZoneSection(pl.x, pl.y, pr.x, pr.y, zoneL);
|
||||
} else {
|
||||
double m = (pr.y - pl.y) / (pr.x - pl.x);
|
||||
int oldZone = 0;
|
||||
if (zoneRange >= 0) {
|
||||
oldZone = zones->whichZone(zoneRange, pl.y);
|
||||
}
|
||||
double oldTime = pl.x;
|
||||
double oldWatts = pl.y;
|
||||
double sectionStartTime = pl.x;
|
||||
double sectionStartWatts = pl.y;
|
||||
for (double i = pl.x; i < pr.x; i += 1000) {
|
||||
double watts = m * (i - pl.x) + pl.y;
|
||||
int newZone = 0;
|
||||
if (zoneRange >= 0) {
|
||||
newZone = zones->whichZone(zoneRange, watts);
|
||||
}
|
||||
if (newZone != oldZone) {
|
||||
ret << ErgFileZoneSection(sectionStartTime, sectionStartWatts, oldTime, oldWatts, oldZone);
|
||||
sectionStartTime = oldTime;
|
||||
sectionStartWatts = watts;
|
||||
oldZone = newZone;
|
||||
}
|
||||
oldTime = i;
|
||||
oldWatts = watts;
|
||||
}
|
||||
ret << ErgFileZoneSection(sectionStartTime, sectionStartWatts, pr.x, pr.y, oldZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
ErgFile::save(QStringList &errors)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,20 @@ class ErgFileSection
|
||||
double start, end;
|
||||
};
|
||||
|
||||
class ErgFileZoneSection
|
||||
: public ErgFileSection
|
||||
{
|
||||
public:
|
||||
ErgFileZoneSection() : ErgFileSection(), startValue(0), endValue(0), zone(0) {}
|
||||
ErgFileZoneSection(int startMSecs, int startValue, int endMSecs, int endValue, int zone)
|
||||
: ErgFileSection(endMSecs - startMSecs, startMSecs, endMSecs), startValue(startValue), endValue(endValue), zone(zone)
|
||||
{}
|
||||
|
||||
int startValue;
|
||||
int endValue;
|
||||
int zone;
|
||||
};
|
||||
|
||||
class ErgFileText
|
||||
{
|
||||
public:
|
||||
@@ -156,6 +170,7 @@ public:
|
||||
// turn the ergfile into a series of sections rather
|
||||
// than a list of points
|
||||
QList<ErgFileSection> Sections();
|
||||
QList<ErgFileZoneSection> ZoneSections();
|
||||
|
||||
QString Version, // version number / identifer
|
||||
Units, // units used
|
||||
|
||||
@@ -21,8 +21,15 @@
|
||||
#include "Context.h"
|
||||
#include "Units.h"
|
||||
|
||||
#include <qwt_picker_machine.h>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
|
||||
static const int sectionAlphaHovered = 128;
|
||||
static const int sectionAlphaNeutral = 255;
|
||||
|
||||
|
||||
// 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
|
||||
@@ -66,7 +73,7 @@ QRectF ErgFileData::boundingRect() const
|
||||
}
|
||||
|
||||
// Now bar
|
||||
double NowData::x(size_t) const {
|
||||
double NowData::x(size_t) const {
|
||||
if (!bydist || GlobalContext::context()->useMetricUnits) return context->getNow();
|
||||
else return context->getNow() * MILES_PER_KM;
|
||||
}
|
||||
@@ -92,6 +99,8 @@ QPointF NowData::sample(size_t i) const
|
||||
|
||||
ErgFilePlot::ErgFilePlot(Context *context) : context(context)
|
||||
{
|
||||
workoutActive = context->isRunning;
|
||||
|
||||
//insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
|
||||
setCanvasBackground(GColor(CTRAINPLOTBACKGROUND));
|
||||
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
|
||||
@@ -161,6 +170,7 @@ ErgFilePlot::ErgFilePlot(Context *context) : context(context)
|
||||
LodCurve = new QwtPlotCurve("Course Load");
|
||||
LodCurve->setSamples(lodData);
|
||||
LodCurve->attach(this);
|
||||
LodCurve->setVisible(workoutActive);
|
||||
LodCurve->setBaseline(-1000);
|
||||
LodCurve->setYAxis(QwtAxis::YLeft);
|
||||
|
||||
@@ -254,6 +264,12 @@ ErgFilePlot::ErgFilePlot(Context *context) : context(context)
|
||||
CPMarker->setYValue(274);
|
||||
CPMarker->attach(this);
|
||||
|
||||
// Dummy curve for ensuring headroom in Ergmode
|
||||
powerHeadroom = new QwtPlotCurve("Dummy Headroom");
|
||||
powerHeadroom->setYAxis(QwtAxis::YLeft);
|
||||
powerHeadroom->setPen(QColor(0, 0, 0, 0));
|
||||
powerHeadroom->attach(this);
|
||||
powerHeadroom->setVisible(false);
|
||||
|
||||
// Now pointer
|
||||
NowCurve = new QwtPlotCurve("Now");
|
||||
@@ -263,17 +279,30 @@ ErgFilePlot::ErgFilePlot(Context *context) : context(context)
|
||||
NowCurve->attach(this);
|
||||
NowCurve->setYAxis(QwtAxis::YLeft);
|
||||
|
||||
tooltip = new penTooltip(static_cast<QwtPlotCanvas*>(canvas()));
|
||||
tooltip->setMousePattern(QwtEventPattern::MouseSelect1, Qt::LeftButton, Qt::ShiftModifier);
|
||||
|
||||
picker = new QwtPlotPicker(QwtAxis::XBottom, QwtAxis::YLeft, canvas());
|
||||
picker->setTrackerMode(QwtPlotPicker::AlwaysOff);
|
||||
picker->setStateMachine(new QwtPickerTrackerMachine());
|
||||
connect(picker, SIGNAL(moved(const QPoint&)), this, SLOT(hover(const QPoint&)));
|
||||
connect(context, SIGNAL(start()), this, SLOT(startWorkout()));
|
||||
connect(context, SIGNAL(stop()), this, SLOT(stopWorkout()));
|
||||
|
||||
bydist = false;
|
||||
ergFile = NULL;
|
||||
|
||||
selectTooltip();
|
||||
|
||||
setAutoReplot(false);
|
||||
setData(ergFile);
|
||||
setData(ergFile);
|
||||
|
||||
configChanged(CONFIG_ZONES);
|
||||
|
||||
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::configChanged(qint32)
|
||||
{
|
||||
@@ -339,7 +368,7 @@ public:
|
||||
//
|
||||
// Build mapping from lapRangeId to index of first/last lap markers in the
|
||||
// group.
|
||||
//
|
||||
//
|
||||
// Map provides instant access to start and end of rangeid.
|
||||
int lapCount = laps.count();
|
||||
|
||||
@@ -417,6 +446,11 @@ void
|
||||
ErgFilePlot::setData(ErgFile *ergfile)
|
||||
{
|
||||
reset();
|
||||
powerHeadroom->setVisible(false);
|
||||
for (int i = 0; i < powerSectionCurves.length(); ++i) {
|
||||
delete powerSectionCurves[i];
|
||||
}
|
||||
powerSectionCurves.clear();
|
||||
|
||||
ergFile = ergfile;
|
||||
// clear the previous marks (if any)
|
||||
@@ -454,8 +488,32 @@ ErgFilePlot::setData(ErgFile *ergfile)
|
||||
LodCurve->setBrush(linearGradient); // fill below the line
|
||||
QPen Lodpen = QPen(Qt::gray, 1.0);
|
||||
LodCurve->setPen(Lodpen);
|
||||
LodCurve->show();
|
||||
|
||||
} else {
|
||||
QList<ErgFileZoneSection> zoneSections = ergFile->ZoneSections();
|
||||
bool antiAlias = appsettings->value(this, GC_ANTIALIAS, false).toBool();
|
||||
for (int i = 0; i < zoneSections.length(); ++i) {
|
||||
QVector<QPointF> sectionData;
|
||||
sectionData << QPointF(zoneSections[i].start, zoneSections[i].startValue)
|
||||
<< QPointF(zoneSections[i].end, zoneSections[i].endValue);
|
||||
QColor color = QColor(zoneColor(zoneSections[i].zone, 0));
|
||||
color.setAlpha(sectionAlphaNeutral);
|
||||
QwtPlotCurve *sectionCurve = new QwtPlotCurve("Course Load");
|
||||
sectionCurve->setSamples(sectionData);
|
||||
sectionCurve->setBaseline(-1000);
|
||||
sectionCurve->setYAxis(QwtAxis::YLeft);
|
||||
sectionCurve->setZ(-100);
|
||||
sectionCurve->setPen(QColor(0, 0, 0, 0));
|
||||
sectionCurve->setBrush(color);
|
||||
sectionCurve->setRenderHint(QwtPlotItem::RenderAntialiased, antiAlias);
|
||||
sectionCurve->attach(this);
|
||||
sectionCurve->hide();
|
||||
powerSectionCurves.append(sectionCurve);
|
||||
}
|
||||
selectCurves();
|
||||
powerHeadroom->setVisible(true);
|
||||
powerHeadroom->setSamples(QVector<QPointF> { dynamic_cast<QwtPointArrayData<double>*>(lodData)->boundingRect().bottomLeft() });
|
||||
|
||||
QColor brush_color1 = QColor(GColor(CTPOWER));
|
||||
brush_color1.setAlpha(200);
|
||||
@@ -470,8 +528,8 @@ ErgFilePlot::setData(ErgFile *ergfile)
|
||||
LodCurve->setBrush(linearGradient); // fill below the line
|
||||
QPen Lodpen = QPen(GColor(CTPOWER), 1.0);
|
||||
LodCurve->setPen(Lodpen);
|
||||
|
||||
}
|
||||
selectTooltip();
|
||||
|
||||
LapRowDistributor lapRowDistributor(ergFile->Laps);
|
||||
|
||||
@@ -512,7 +570,7 @@ ErgFilePlot::setData(ErgFile *ergfile)
|
||||
// Literal row translation. We loves ascii art...
|
||||
QString prefix = (row > 0) ? QString("\n").repeated(row) : "";
|
||||
QwtText text(prefix + decoratedName);
|
||||
|
||||
|
||||
text.setFont(QFont("Helvetica", 10, QFont::Bold));
|
||||
text.setColor(GColor(CPLOTMARKER));
|
||||
|
||||
@@ -524,7 +582,7 @@ ErgFilePlot::setData(ErgFile *ergfile)
|
||||
// convert to imperial according to settings
|
||||
double unitsFactor = (!bydist || GlobalContext::context()->useMetricUnits) ? 1.0 : MILES_PER_KM;
|
||||
add->setValue(lap.x * unitsFactor, 0);
|
||||
|
||||
|
||||
add->setLabel(text);
|
||||
add->attach(this);
|
||||
|
||||
@@ -619,6 +677,53 @@ ErgFilePlot::setNow(long /*msecs*/)
|
||||
replot(); // and update
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
ErgFilePlot::eventFilter
|
||||
(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj == canvas() && event->type() == QEvent::Leave) {
|
||||
highlightSectionCurve(nullptr);
|
||||
tooltip->setText("");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
ErgFilePlot::showColorZones
|
||||
() const
|
||||
{
|
||||
return _showColorZones;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::setShowColorZones
|
||||
(int index)
|
||||
{
|
||||
_showColorZones = index;
|
||||
selectCurves();
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
ErgFilePlot::showTooltip
|
||||
() const
|
||||
{
|
||||
return _showTooltip;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::setShowTooltip
|
||||
(int index)
|
||||
{
|
||||
_showTooltip = index;
|
||||
selectTooltip();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::performancePlot(RealtimeData rtdata)
|
||||
{
|
||||
@@ -714,6 +819,221 @@ ErgFilePlot::reset()
|
||||
speedCurve->setSamples(speedData->x(), speedData->y(), speedData->count());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::hover
|
||||
(const QPoint &point)
|
||||
{
|
||||
if ( bydist
|
||||
|| _showTooltip == 0
|
||||
|| ( _showTooltip == 1
|
||||
&& workoutActive)
|
||||
|| ergFile == nullptr
|
||||
|| ergFile->Duration == 0) {
|
||||
tooltip->setText("");
|
||||
return;
|
||||
}
|
||||
double xvalue = invTransform(QwtAxis::XBottom, point.x());
|
||||
double yvalue = invTransform(QwtAxis::YLeft, point.y());
|
||||
const int fullSecs = std::min(std::max(0, int(xvalue)), int(ergFile->Duration) - 1000) / 1000;
|
||||
int duration = 0;
|
||||
int startPower = 0;
|
||||
int endPower = 0;
|
||||
QwtPlotCurve *hoverCurve = nullptr;
|
||||
for (QwtPlotCurve*& curve : powerSectionCurves) {
|
||||
if (curve->minXValue() <= xvalue && xvalue <= curve->maxXValue()) {
|
||||
duration = (curve->maxXValue() - curve->minXValue()) / 1000;
|
||||
startPower = curve->sample(0).y();
|
||||
endPower = curve->sample(1).y();
|
||||
hoverCurve = curve;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int watts = startPower;
|
||||
if (hoverCurve != nullptr && startPower != endPower) {
|
||||
QPointF pl = hoverCurve->sample(0);
|
||||
QPointF pr = hoverCurve->sample(1);
|
||||
watts = (pr.y() - pl.y()) / (pr.x() - pl.x()) * (xvalue - pl.x()) + pl.y();
|
||||
}
|
||||
if (watts == 0 || yvalue > watts) {
|
||||
highlightSectionCurve(nullptr);
|
||||
tooltip->setText("");
|
||||
return;
|
||||
}
|
||||
if (hoverCurve->brush().color().alpha() != sectionAlphaNeutral) {
|
||||
return;
|
||||
}
|
||||
|
||||
double sectionStart = hoverCurve->sample(0).x();
|
||||
double sectionEnd = hoverCurve->sample(1).x();
|
||||
|
||||
highlightSectionCurve(hoverCurve);
|
||||
QString tooltipText;
|
||||
tooltipText = QString("%1\n%4: ")
|
||||
.arg(tr("Section of %1 starts at %2")
|
||||
.arg(secsToString(duration))
|
||||
.arg(secsToString(sectionStart / 1000)))
|
||||
.arg(tr("Power"));
|
||||
if (startPower == endPower) {
|
||||
tooltipText = QString("%1%2 %3")
|
||||
.arg(tooltipText)
|
||||
.arg(startPower)
|
||||
.arg(tr("watts"));
|
||||
} else {
|
||||
tooltipText = QString("%1%2..%3 %4")
|
||||
.arg(tooltipText)
|
||||
.arg(startPower)
|
||||
.arg(endPower)
|
||||
.arg(tr("watts"));
|
||||
}
|
||||
const Zones *zones = context->athlete->zones("Bike");
|
||||
int zoneRange = zones->whichRange(QDate::currentDate());
|
||||
if (zoneRange >= 0) {
|
||||
tooltipText = QString("%1 (%2)")
|
||||
.arg(tooltipText)
|
||||
.arg(zones->getZoneNames(zoneRange)[zones->whichZone(zoneRange, startPower)]);
|
||||
}
|
||||
if (wbalCurvePredict != nullptr && wbalCurvePredict->dataSize() >= fullSecs) {
|
||||
int secsStart = std::min(int(sectionStart / 1000), int(wbalCurvePredict->dataSize() - 1));
|
||||
int secsEnd = std::min(int(sectionEnd / 1000), int(wbalCurvePredict->dataSize() - 1));
|
||||
double wbalIn = wbalCurvePredict->sample(secsStart).y();
|
||||
double wbalOut = wbalCurvePredict->sample(secsEnd).y();
|
||||
if (int(wbalIn / 100) == int(wbalOut / 100)) {
|
||||
tooltipText = QString("%1\n%2: %3 %4")
|
||||
.arg(tooltipText)
|
||||
.arg(tr("W' Balance"))
|
||||
.arg(wbalIn / 1000.0 , 0, 'f', 1)
|
||||
.arg(tr("kJ"));
|
||||
} else {
|
||||
tooltipText = QString("%1\n%2: %3..%4 (%5) %6")
|
||||
.arg(tooltipText)
|
||||
.arg(tr("W' Balance"))
|
||||
.arg(wbalIn / 1000.0 , 0, 'f', 1)
|
||||
.arg(wbalOut / 1000.0 , 0, 'f', 1)
|
||||
.arg((wbalOut - wbalIn) / 1000.0 , 0, 'f', 1)
|
||||
.arg(tr("kJ"));
|
||||
}
|
||||
}
|
||||
tooltip->setText(tooltipText);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::startWorkout
|
||||
()
|
||||
{
|
||||
workoutActive = true;
|
||||
selectTooltip();
|
||||
selectCurves();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::stopWorkout
|
||||
()
|
||||
{
|
||||
workoutActive = false;
|
||||
selectCurves();
|
||||
selectTooltip();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::selectCurves
|
||||
()
|
||||
{
|
||||
bool showColored = ergFile
|
||||
&& ! bydist
|
||||
&& ( _showColorZones == 1
|
||||
|| (_showColorZones == 2 && ! workoutActive));
|
||||
if (showColored) {
|
||||
LodCurve->hide();
|
||||
for (int i = 0; i < powerSectionCurves.size(); ++i) {
|
||||
powerSectionCurves[i]->show();
|
||||
}
|
||||
} else {
|
||||
LodCurve->show();
|
||||
for (int i = 0; i < powerSectionCurves.size(); ++i) {
|
||||
powerSectionCurves[i]->hide();
|
||||
}
|
||||
}
|
||||
replot();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::selectTooltip
|
||||
()
|
||||
{
|
||||
if ( ergFile
|
||||
&& ! bydist
|
||||
&& (_showTooltip == 1 && ! workoutActive)) {
|
||||
installEventFilter(canvas());
|
||||
picker->setEnabled(true);
|
||||
} else {
|
||||
removeEventFilter(canvas());
|
||||
picker->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QString
|
||||
ErgFilePlot::secsToString
|
||||
(int fullSecs) const
|
||||
{
|
||||
int secs = fullSecs % 60;
|
||||
int mins = (fullSecs / 60) % 60;
|
||||
int hours = fullSecs / 3600;
|
||||
QString time;
|
||||
if (hours > 0) {
|
||||
if (secs > 0) {
|
||||
time = QString("%1h %2m %3s").arg(hours).arg(mins).arg(secs);
|
||||
} else {
|
||||
time = QString("%1h %2m").arg(hours).arg(mins);
|
||||
}
|
||||
} else if (mins > 0) {
|
||||
if (secs > 0) {
|
||||
time = QString("%1m %2s").arg(mins).arg(secs);
|
||||
} else {
|
||||
time = QString("%2m").arg(mins);
|
||||
}
|
||||
} else {
|
||||
time = QString("%1s").arg(secs);
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErgFilePlot::highlightSectionCurve
|
||||
(QwtPlotCurve const * const highlightedCurve)
|
||||
{
|
||||
bool needsReplot = false;
|
||||
for (QwtPlotCurve*& curve : powerSectionCurves) {
|
||||
QBrush brush = curve->brush();
|
||||
QColor color = brush.color();
|
||||
if (curve != highlightedCurve) {
|
||||
if (color.alpha() != sectionAlphaNeutral) {
|
||||
color.setAlpha(sectionAlphaNeutral);
|
||||
brush.setColor(color);
|
||||
curve->setBrush(brush);
|
||||
needsReplot = true;
|
||||
}
|
||||
} else {
|
||||
if (color.alpha() == sectionAlphaNeutral) {
|
||||
color.setAlpha(sectionAlphaHovered);
|
||||
brush.setColor(color);
|
||||
curve->setBrush(brush);
|
||||
needsReplot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needsReplot) {
|
||||
replot();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// curve data.. code snaffled in from the Qwt example (realtime_plot)
|
||||
CurveData::CurveData(): d_count(0) { }
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
#include "Settings.h"
|
||||
#include "Colors.h"
|
||||
#include "PowerHist.h" // for penTooltip
|
||||
|
||||
#include "RealtimeData.h"
|
||||
#include <qwt_series_data.h>
|
||||
@@ -50,83 +51,89 @@
|
||||
class ErgFileData : public QwtPointArrayData<double>
|
||||
{
|
||||
public:
|
||||
ErgFileData (Context *context) : QwtPointArrayData(QVector<double>(), QVector<double>()), context(context) {}
|
||||
double x(size_t i) const ;
|
||||
double y(size_t i) const ;
|
||||
size_t size() const ;
|
||||
void setByDist(bool bd) { bydist = bd; };
|
||||
bool byDist() const { return bydist; };
|
||||
ErgFileData (Context *context) : QwtPointArrayData(QVector<double>(), QVector<double>()), context(context) {}
|
||||
double x(size_t i) const;
|
||||
double y(size_t i) const;
|
||||
size_t size() const;
|
||||
void setByDist(bool bd) { bydist = bd; };
|
||||
bool byDist() const { return bydist; };
|
||||
|
||||
private:
|
||||
Context *context;
|
||||
bool bydist = false;
|
||||
Context *context;
|
||||
bool bydist = false;
|
||||
|
||||
virtual QPointF sample(size_t i) const;
|
||||
virtual QRectF boundingRect() const;
|
||||
virtual QPointF sample(size_t i) const;
|
||||
virtual QRectF boundingRect() const;
|
||||
};
|
||||
|
||||
|
||||
class NowData : public QwtPointArrayData<double>
|
||||
{
|
||||
public:
|
||||
NowData (Context *context) : QwtPointArrayData(QVector<double>(), QVector<double>()), context(context) {}
|
||||
double x(size_t i) const ;
|
||||
double y(size_t i) const ;
|
||||
size_t size() const ;
|
||||
void setByDist(bool bd) { bydist = bd; };
|
||||
bool byDist() const { return bydist; };
|
||||
NowData (Context *context) : QwtPointArrayData(QVector<double>(), QVector<double>()), context(context) {}
|
||||
double x(size_t i) const;
|
||||
double y(size_t i) const;
|
||||
size_t size() const;
|
||||
void setByDist(bool bd) { bydist = bd; };
|
||||
bool byDist() const { return bydist; };
|
||||
|
||||
void init();
|
||||
|
||||
void init() ;
|
||||
private:
|
||||
Context *context;
|
||||
bool bydist = false;
|
||||
Context *context;
|
||||
bool bydist = false;
|
||||
|
||||
virtual QPointF sample(size_t i) const;
|
||||
//virtual QRectF boundingRect() const;
|
||||
virtual QPointF sample(size_t i) const;
|
||||
//virtual QRectF boundingRect() const;
|
||||
};
|
||||
|
||||
|
||||
// incremental data, for each curve
|
||||
class CurveData
|
||||
{
|
||||
// A container class for growing data
|
||||
public:
|
||||
public:
|
||||
|
||||
CurveData();
|
||||
CurveData();
|
||||
|
||||
void append(double *x, double *y, int count);
|
||||
void clear();
|
||||
void append(double *x, double *y, int count);
|
||||
void clear();
|
||||
|
||||
int count() const;
|
||||
int size() const;
|
||||
const double *x() const;
|
||||
const double *y() const;
|
||||
int count() const;
|
||||
int size() const;
|
||||
const double *x() const;
|
||||
const double *y() const;
|
||||
|
||||
private:
|
||||
int d_count;
|
||||
QVector<double> d_x;
|
||||
QVector<double> d_y;
|
||||
private:
|
||||
int d_count;
|
||||
QVector<double> d_x;
|
||||
QVector<double> d_y;
|
||||
};
|
||||
|
||||
|
||||
class DistScaleDraw: public QwtScaleDraw
|
||||
{
|
||||
public:
|
||||
DistScaleDraw() { }
|
||||
public:
|
||||
DistScaleDraw() { }
|
||||
|
||||
// we do 100m for <= a kilo
|
||||
virtual QwtText label(double v) const { if (v<1000) return QString("%1").arg(v/1000, 0, 'g', 1);
|
||||
else return QString("%1").arg(round(v/1000)); }
|
||||
// we do 100m for <= a kilo
|
||||
virtual QwtText label(double v) const { if (v<1000) return QString("%1").arg(v/1000, 0, 'g', 1);
|
||||
else return QString("%1").arg(round(v/1000)); }
|
||||
};
|
||||
|
||||
|
||||
class HourTimeScaleDraw: public QwtScaleDraw
|
||||
{
|
||||
public:
|
||||
HourTimeScaleDraw() { }
|
||||
public:
|
||||
HourTimeScaleDraw() { }
|
||||
|
||||
virtual QwtText label(double v) const {
|
||||
v /= 1000;
|
||||
QTime t = QTime(0,0,0,0).addSecs(v);
|
||||
if (scaleMap().sDist() > 5)
|
||||
return t.toString("hh:mm");
|
||||
return t.toString("hh:mm:ss");
|
||||
}
|
||||
virtual QwtText label(double v) const {
|
||||
v /= 1000;
|
||||
QTime t = QTime(0,0,0,0).addSecs(v);
|
||||
if (scaleMap().sDist() > 5)
|
||||
return t.toString("hh:mm");
|
||||
return t.toString("hh:mm:ss");
|
||||
}
|
||||
};
|
||||
|
||||
class ErgFilePlot : public QwtPlot
|
||||
@@ -134,64 +141,81 @@ class ErgFilePlot : public QwtPlot
|
||||
Q_OBJECT
|
||||
G_OBJECT
|
||||
|
||||
|
||||
public:
|
||||
ErgFilePlot(Context *context);
|
||||
|
||||
ErgFilePlot(Context *context);
|
||||
QList<QwtPlotMarker *> Marks;
|
||||
QList<QwtPlotMarker *> Marks;
|
||||
|
||||
void setData(ErgFile *); // set the course
|
||||
void reset(); // reset counters etc when plot changes
|
||||
void setNow(long); // set point we're add for progress pointer
|
||||
void setData(ErgFile *); // set the course
|
||||
void reset(); // reset counters etc when plot changes
|
||||
void setNow(long); // set point we're add for progress pointer
|
||||
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
||||
public slots:
|
||||
void performancePlot(RealtimeData);
|
||||
void configChanged(qint32);
|
||||
void start();
|
||||
void hover(const QPoint &point);
|
||||
void startWorkout();
|
||||
void stopWorkout();
|
||||
void selectCurves();
|
||||
void selectTooltip();
|
||||
|
||||
void performancePlot(RealtimeData);
|
||||
void configChanged(qint32);
|
||||
void start();
|
||||
int showColorZones() const;
|
||||
void setShowColorZones(int index);
|
||||
int showTooltip() const;
|
||||
void setShowTooltip(int index);
|
||||
|
||||
private:
|
||||
WPrime calculator;
|
||||
Context *context;
|
||||
bool bydist;
|
||||
ErgFile *ergFile;
|
||||
QwtPlotMarker *CPMarker;
|
||||
|
||||
WPrime calculator;
|
||||
Context *context;
|
||||
bool bydist;
|
||||
ErgFile *ergFile;
|
||||
QwtPlotMarker *CPMarker;
|
||||
int _showColorZones = 0;
|
||||
int _showTooltip = 0;
|
||||
|
||||
QwtPlotGrid *grid;
|
||||
QwtPlotCurve *LodCurve;
|
||||
QwtPlotCurve *wbalCurve;
|
||||
QwtPlotCurve *wbalCurvePredict;
|
||||
QwtPlotCurve *wattsCurve;
|
||||
QwtPlotCurve *hrCurve;
|
||||
QwtPlotCurve *cadCurve;
|
||||
QwtPlotCurve *speedCurve;
|
||||
QwtPlotCurve *NowCurve;
|
||||
QwtPlotGrid *grid;
|
||||
QList<QwtPlotCurve*> powerSectionCurves;
|
||||
QwtPlotCurve *LodCurve;
|
||||
QwtPlotCurve *wbalCurve;
|
||||
QwtPlotCurve *wbalCurvePredict;
|
||||
QwtPlotCurve *wattsCurve;
|
||||
QwtPlotCurve *hrCurve;
|
||||
QwtPlotCurve *cadCurve;
|
||||
QwtPlotCurve *speedCurve;
|
||||
QwtPlotCurve *NowCurve;
|
||||
QwtPlotCurve *powerHeadroom;
|
||||
|
||||
CurveData *wattsData,
|
||||
*hrData,
|
||||
*cadData,
|
||||
*wbalData,
|
||||
*speedData;
|
||||
CurveData *wattsData,
|
||||
*hrData,
|
||||
*cadData,
|
||||
*wbalData,
|
||||
*speedData;
|
||||
|
||||
double counter;
|
||||
double wattssum,
|
||||
hrsum,
|
||||
cadsum,
|
||||
wbalsum,
|
||||
speedsum;
|
||||
double counter;
|
||||
double wattssum,
|
||||
hrsum,
|
||||
cadsum,
|
||||
wbalsum,
|
||||
speedsum;
|
||||
|
||||
ErgFileData *lodData;
|
||||
NowData *nowData;
|
||||
ErgFileData *lodData;
|
||||
NowData *nowData;
|
||||
|
||||
DistScaleDraw *distdraw;
|
||||
HourTimeScaleDraw *timedraw;
|
||||
DistScaleDraw *distdraw;
|
||||
HourTimeScaleDraw *timedraw;
|
||||
|
||||
ErgFilePlot();
|
||||
QwtPlotPicker *picker;
|
||||
penTooltip *tooltip;
|
||||
bool workoutActive = false;
|
||||
|
||||
void highlightSectionCurve(QwtPlotCurve const * const highlightedCurve);
|
||||
QString secsToString(int fullSecs) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -21,14 +21,45 @@
|
||||
#include "Context.h"
|
||||
#include "HelpWhatsThis.h"
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
|
||||
|
||||
WorkoutPlotWindow::WorkoutPlotWindow(Context *context) :
|
||||
GcChartWindow(context), context(context)
|
||||
{
|
||||
HelpWhatsThis *helpContents = new HelpWhatsThis(this);
|
||||
this->setWhatsThis(helpContents->getWhatsThisText(HelpWhatsThis::ChartTrain_Workout));
|
||||
|
||||
// Chart settings
|
||||
QWidget *settingsWidget = new QWidget(this);
|
||||
settingsWidget->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QVBoxLayout *commonLayout = new QVBoxLayout(settingsWidget);
|
||||
|
||||
ctrlsGroupBox = new QGroupBox(tr("Ergmode specific settings"));
|
||||
commonLayout->addWidget(ctrlsGroupBox);
|
||||
commonLayout->addStretch();
|
||||
|
||||
QFormLayout *ergmodeLayout = new QFormLayout(ctrlsGroupBox);
|
||||
|
||||
ctrlsSituationLabel = new QLabel(tr("Color power zones"));
|
||||
ctrlsSituation = new QComboBox();
|
||||
ctrlsSituation->addItem(tr("Never"));
|
||||
ctrlsSituation->addItem(tr("Always"));
|
||||
ctrlsSituation->addItem(tr("When stopped"));
|
||||
connect(ctrlsSituation, SIGNAL(currentIndexChanged(int)), this, SLOT(setShowColorZones(int)));
|
||||
ergmodeLayout->addRow(ctrlsSituationLabel, ctrlsSituation);
|
||||
|
||||
ctrlsShowTooltipLabel = new QLabel(tr("Show tooltip"));
|
||||
ctrlsShowTooltip = new QComboBox();
|
||||
ctrlsShowTooltip->addItem(tr("Never"));
|
||||
ctrlsShowTooltip->addItem(tr("When stopped"));
|
||||
connect(ctrlsShowTooltip, SIGNAL(currentIndexChanged(int)), this, SLOT(setShowTooltip(int)));
|
||||
ergmodeLayout->addRow(ctrlsShowTooltipLabel, ctrlsShowTooltip);
|
||||
|
||||
setContentsMargins(0,0,0,0);
|
||||
setControls(NULL);
|
||||
setControls(settingsWidget);
|
||||
setProperty("color", GColor(CTRAINPLOTBACKGROUND));
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout;
|
||||
@@ -69,5 +100,50 @@ void
|
||||
WorkoutPlotWindow::configChanged(qint32)
|
||||
{
|
||||
setProperty("color", GColor(CTRAINPLOTBACKGROUND));
|
||||
|
||||
ctrlsGroupBox->setTitle(tr("Ergmode specific settings"));
|
||||
ctrlsSituationLabel->setText(tr("Color power zones"));
|
||||
ctrlsSituation->setItemText(0, tr("Never"));
|
||||
ctrlsSituation->setItemText(1, tr("Always"));
|
||||
ctrlsSituation->setItemText(2, tr("When stopped"));
|
||||
|
||||
ctrlsShowTooltipLabel->setText(tr("Show tooltip"));
|
||||
ctrlsShowTooltip->setItemText(0, tr("Never"));
|
||||
ctrlsShowTooltip->setItemText(1, tr("When stopped"));
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
WorkoutPlotWindow::showColorZones
|
||||
() const
|
||||
{
|
||||
return ctrlsSituation->currentIndex();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WorkoutPlotWindow::setShowColorZones
|
||||
(int index)
|
||||
{
|
||||
ctrlsSituation->setCurrentIndex(index);
|
||||
ergPlot->setShowColorZones(index);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
WorkoutPlotWindow::showTooltip
|
||||
() const
|
||||
{
|
||||
return ctrlsShowTooltip->currentIndex();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WorkoutPlotWindow::setShowTooltip
|
||||
(int index)
|
||||
{
|
||||
ctrlsShowTooltip->setCurrentIndex(index);
|
||||
ergPlot->setShowTooltip(index);
|
||||
}
|
||||
|
||||
@@ -32,26 +32,44 @@
|
||||
#include "Settings.h"
|
||||
#include "Colors.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
|
||||
class WorkoutPlotWindow : public GcChartWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
G_OBJECT
|
||||
|
||||
Q_PROPERTY(int showColorZones READ showColorZones WRITE setShowColorZones USER true)
|
||||
Q_PROPERTY(int showTooltip READ showTooltip WRITE setShowTooltip USER true)
|
||||
|
||||
public:
|
||||
|
||||
WorkoutPlotWindow(Context *context);
|
||||
|
||||
public slots:
|
||||
public slots:
|
||||
|
||||
// trap signals
|
||||
void setNow(long now);
|
||||
void ergFileSelected(ErgFile *);
|
||||
void configChanged(qint32);
|
||||
|
||||
int showColorZones() const;
|
||||
void setShowColorZones(int index);
|
||||
int showTooltip() const;
|
||||
void setShowTooltip(int index);
|
||||
|
||||
private:
|
||||
|
||||
Context *context;
|
||||
ErgFilePlot *ergPlot;
|
||||
|
||||
QGroupBox *ctrlsGroupBox;
|
||||
QLabel *ctrlsSituationLabel;
|
||||
QComboBox *ctrlsSituation;
|
||||
QLabel *ctrlsShowTooltipLabel;
|
||||
QComboBox *ctrlsShowTooltip;
|
||||
};
|
||||
|
||||
#endif // _GC_WorkoutPlotWindow_h
|
||||
|
||||
Reference in New Issue
Block a user