Files
GoldenCheetah/src/PowerHist.cpp
Joern 7222182bf7 Translation Enablement & "seconds"
... more tr() changes
... added comparison for  both "seconds" OR tr("seconds") to all other
occurences found
... added "Lap" text for Intervalls imported in Garmin FIT format (this
is what Garmin delivers)

(cherry picked from commit ad790f05a386e3ae5c5b9dc129a216ac451b8cfc)
2014-06-08 17:55:24 +02:00

1994 lines
66 KiB
C++

/*
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
* 2011 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 "PowerHist.h"
#include "MainWindow.h"
#include "Context.h"
#include "Athlete.h"
#include "RideItem.h"
#include "IntervalItem.h"
#include "RideFile.h"
#include "RideFileCache.h"
#include "SummaryMetrics.h"
#include "Settings.h"
#include "Zones.h"
#include "HrZones.h"
#include "Colors.h"
#include "ZoneScaleDraw.h"
#include <qpainter.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_zoomer.h>
#include <qwt_scale_engine.h>
#include <qwt_scale_widget.h>
#include <qwt_text.h>
#include <qwt_legend.h>
#include <qwt_series_data.h>
#include "LTMCanvasPicker.h" // for tooltip
PowerHist::PowerHist(Context *context, bool rangemode) :
minX(0),
maxX(0),
rangemode(rangemode),
rideItem(NULL),
context(context),
series(RideFile::watts),
lny(false),
shade(false),
zoned(false),
cpzoned(false),
binw(3),
withz(true),
dt(1),
absolutetime(true),
cache(NULL),
source(Ride)
{
binw = appsettings->value(this, GC_HIST_BIN_WIDTH, 5).toInt();
if (appsettings->value(this, GC_SHADEZONES, true).toBool() == true)
shade = true;
else
shade = false;
// create a background object for shading
bg = new PowerHistBackground(this);
bg->attach(this);
hrbg = new HrHistBackground(this);
hrbg->attach(this);
setCanvasBackground(Qt::white);
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
setParameterAxisTitle();
setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)"));
curve = new QwtPlotCurve("");
curve->setStyle(QwtPlotCurve::Steps);
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
curve->setZ(20);
curve->attach(this);
curveSelected = new QwtPlotCurve("");
curveSelected->setStyle(QwtPlotCurve::Steps);
curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased);
curveSelected->setZ(50);
curveSelected->attach(this);
curveHover = new QwtPlotCurve("");
curveHover->setStyle(QwtPlotCurve::Steps);
curveHover->setRenderHint(QwtPlotItem::RenderAntialiased);
curveHover->setZ(100);
curveHover->attach(this);
grid = new QwtPlotGrid();
grid->enableX(false);
grid->attach(this);
zoneLabels = QList<PowerHistZoneLabel *>();
hrzoneLabels = QList<HrHistZoneLabel *>();
zoomer = new penTooltip(this->canvas());
canvasPicker = new LTMCanvasPicker(this);
connect(canvasPicker, SIGNAL(pointHover(QwtPlotCurve*, int)), this, SLOT(pointHover(QwtPlotCurve*, int)));
// usually hidden, but shown for compare mode
//XXX insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
setAxisMaxMinor(xBottom, 0);
setAxisMaxMinor(yLeft, 0);
configChanged();
}
void
PowerHist::configChanged()
{
// plot background
setCanvasBackground(GColor(CPLOTBACKGROUND));
// curve
QPen pen;
QColor brush_color;
if (source == Metric) {
pen.setColor(metricColor.darker(200));
brush_color = metricColor;
} else {
switch (series) {
case RideFile::watts:
case RideFile::aPower:
case RideFile::wattsKg:
pen.setColor(GColor(CPOWER).darker(200));
brush_color = GColor(CPOWER);
break;
case RideFile::nm:
pen.setColor(GColor(CTORQUE).darker(200));
brush_color = GColor(CTORQUE);
break;
case RideFile::kph:
pen.setColor(GColor(CSPEED).darker(200));
brush_color = GColor(CSPEED);
break;
case RideFile::cad:
pen.setColor(GColor(CCADENCE).darker(200));
brush_color = GColor(CCADENCE);
break;
default:
case RideFile::hr:
pen.setColor(GColor(CHEARTRATE).darker(200));
brush_color = GColor(CHEARTRATE);
break;
}
}
double width = appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble();
if (appsettings->value(this, GC_ANTIALIAS, false).toBool()==true) {
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased);
curveHover->setRenderHint(QwtPlotItem::RenderAntialiased);
}
// use a linear gradient
brush_color.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200);
QColor brush_color1 = brush_color.darker();
QLinearGradient linearGradient(0, 0, 0, height());
linearGradient.setColorAt(0.0, brush_color);
linearGradient.setColorAt(1.0, brush_color1);
linearGradient.setSpread(QGradient::PadSpread);
curve->setBrush(linearGradient); // fill below the line
if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::wattsKg && series != RideFile::hr))) {
pen.setWidth(width);
curve->setPen(pen);
} else {
pen.setWidth(width);
curve->setPen(QPen(Qt::NoPen));
}
// intervalselection
QPen ivl(GColor(CINTERVALHIGHLIGHTER).darker(200));
ivl.setWidth(width);
curveSelected->setPen(ivl);
QColor ivlbrush = GColor(CINTERVALHIGHLIGHTER);
ivlbrush.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200);
curveSelected->setBrush(ivlbrush); // fill below the line
// hover curve
QPen hvl(Qt::darkGray);
hvl.setWidth(width);
curveHover->setPen(hvl);
QColor hvlbrush = QColor(Qt::darkGray);
hvlbrush.setAlpha((GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 64 : 200));
curveHover->setBrush(hvlbrush); // fill below the line
// grid
QPen gridPen(GColor(CPLOTGRID));
//gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
QPalette palette;
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
setPalette(palette);
axisWidget(QwtPlot::xBottom)->setPalette(palette);
axisWidget(QwtPlot::yLeft)->setPalette(palette);
setAutoFillBackground(true);
}
void
PowerHist::hideStandard(bool hide)
{
bg->setVisible(!hide);
hrbg->setVisible(!hide);
curve->setVisible(!hide);
curveSelected->setVisible(!hide);
curveHover->setVisible(!hide);
// clear if we are showing standard (not comparing) or if the comparisons got cleared
if (!hide || (rangemode && context->compareDateRanges.count() == 0) || (!rangemode && context->compareIntervals.count() == 0) ) {
// wipe the compare data
compareData.clear();
// we want normal so zap any compare curves
foreach(QwtPlotCurve *x, compareCurves) {
x->detach();
delete x;
}
compareCurves.clear();
}
}
PowerHist::~PowerHist() {
delete bg;
delete hrbg;
delete curve;
delete curveSelected;
delete curveHover;
delete grid;
}
void
PowerHist::refreshZoneLabels()
{
// delete any existing power zone labels
if (zoneLabels.size()) {
QListIterator<PowerHistZoneLabel *> i(zoneLabels);
while (i.hasNext()) {
PowerHistZoneLabel *label = i.next();
label->detach();
delete label;
}
}
zoneLabels.clear();
if (!rideItem) return;
if (series == RideFile::watts || series == RideFile::wattsKg) {
const Zones *zones = rideItem->zones;
int zone_range = rideItem->zoneRange();
// generate labels for existing zones
if (zone_range >= 0) {
int num_zones = zones->numZones(zone_range);
for (int z = 0; z < num_zones; z ++) {
PowerHistZoneLabel *label = new PowerHistZoneLabel(this, z);
label->attach(this);
zoneLabels.append(label);
}
}
}
}
void
PowerHist::refreshHRZoneLabels()
{
// delete any existing power zone labels
if (hrzoneLabels.size()) {
QListIterator<HrHistZoneLabel *> i(hrzoneLabels);
while (i.hasNext()) {
HrHistZoneLabel *label = i.next();
label->detach();
delete label;
}
}
hrzoneLabels.clear();
if (!rideItem) return;
if (series == RideFile::hr) {
const HrZones *zones = context->athlete->hrZones();
int zone_range = rideItem->hrZoneRange();
// generate labels for existing zones
if (zone_range >= 0) {
int num_zones = zones->numZones(zone_range);
for (int z = 0; z < num_zones; z ++) {
HrHistZoneLabel *label = new HrHistZoneLabel(this, z);
label->attach(this);
hrzoneLabels.append(label);
}
}
}
}
void
PowerHist::recalcCompare()
{
// Set curves .. they will always have been created
// in setDataFromCompareIntervals, but no samples set
// don't bother if not comparing for our mode or we're not visible
if (!isVisible() ||
((rangemode && !context->isCompareDateRanges)
|| (!rangemode && !context->isCompareIntervals))) return;
// zap any zone data labels
foreach (QwtPlotMarker *label, zoneDataLabels) {
label->detach();
delete label;
}
zoneDataLabels.clear();
// loop through intervals or dateranges, depending upon mode
// counting columns
double ncols = 0;
for(int i=0; (rangemode && i < context->compareDateRanges.count()) ||
(!rangemode && i < context->compareIntervals.count()) ; i++) {
if (rangemode && context->compareDateRanges[i].isChecked()) ncols++;
if (!rangemode && context->compareIntervals[i].isChecked()) ncols++;
}
int acol = 0;
int maxX = 0;
// loop again but now setting up data
for(int intervalNumber=0; (rangemode && intervalNumber < context->compareDateRanges.count()) ||
(!rangemode && intervalNumber < context->compareIntervals.count()) ; intervalNumber++) {
HistData &cid = compareData[intervalNumber];
QwtPlotCurve *curve = compareCurves[intervalNumber];
QVector<unsigned int> *array = NULL;
int arrayLength = 0;
if (source == Metric) {
// we use the metricArray
array = &cid.metricArray;
arrayLength = cid.metricArray.size();
} else if (series == RideFile::watts && zoned == false) {
array = &cid.wattsArray;
arrayLength = cid.wattsArray.size();
} else if ((series == RideFile::watts || series == RideFile::wattsKg) && zoned == true) {
if (cpzoned) {
array = &cid.wattsCPZoneArray;
arrayLength = cid.wattsCPZoneArray.size();
} else {
array = &cid.wattsZoneArray;
arrayLength = cid.wattsZoneArray.size();
}
} else if (series == RideFile::aPower && zoned == false) {
array = &cid.aPowerArray;
arrayLength = cid.aPowerArray.size();
} else if (series == RideFile::wattsKg && zoned == false) {
array = &cid.wattsKgArray;
arrayLength = cid.wattsKgArray.size();
} else if (series == RideFile::nm) {
array = &cid.nmArray;
arrayLength = cid.nmArray.size();
} else if (series == RideFile::hr && zoned == false) {
array = &cid.hrArray;
arrayLength = cid.hrArray.size();
} else if (series == RideFile::hr && zoned == true) {
array = &cid.hrZoneArray;
arrayLength = cid.hrZoneArray.size();
} else if (series == RideFile::kph) {
array = &cid.kphArray;
arrayLength = cid.kphArray.size();
} else if (series == RideFile::cad) {
array = &cid.cadArray;
arrayLength = cid.cadArray.size();
}
// UNUSEDRideFile::SeriesType baseSeries = (series == RideFile::wattsKg) ? RideFile::watts : series;
// null curve please -- we have no data!
if (!array || arrayLength == 0) {
// create empty curves when no data
const double zero = 0;
curve->setSamples(&zero, &zero, 0);
continue;
}
if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::wattsKg
&& series != RideFile::hr))) {
// NOT ZONED
// we add a bin on the end since the last "incomplete" bin
// will be dropped otherwise
int count = int(ceil((arrayLength - 1) / (binw)))+1;
// allocate space for data, plus beginning and ending point
QVector<double> parameterValue(count+2, 0.0);
QVector<double> totalTime(count+2, 0.0);
int i;
for (i = 1; i <= count; ++i) {
double high = i * round(binw/delta);
double low = high - round(binw/delta);
if (low==0 && !withz) low++;
parameterValue[i] = high*delta;
totalTime[i] = 1e-9; // nonzero to accomodate log plot
while (low < high && low<arrayLength) {
totalTime[i] += dt * (*array)[low++];
}
}
totalTime[i] = 1e-9; // nonzero to accomodate log plot
parameterValue[i] = i * delta * binw;
totalTime[0] = 1e-9;
parameterValue[0] = 0;
// convert vectors from absolute time to percentage
// if the user has selected that
if (!absolutetime) {
percentify(totalTime, 1);
}
curve->setSamples(parameterValue.data(), totalTime.data(), count + 2);
QwtScaleDraw *sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
setAxisScaleDraw(QwtPlot::xBottom, sd);
// HR typically starts at 80 or so, rather than zero
// lets crop the chart so we can focus on the data
// if we're working with HR data...
minX=0;
if (!withz && series == RideFile::hr) {
for (int i=1; i<cid.hrArray.size(); i++) {
if (cid.hrArray[i] > 0.1) {
minX = i;
break;
}
}
}
// only set X-axis to largest value with significant value
int truncate = count;
while (truncate > 0) {
if (!absolutetime && totalTime[truncate] >= 0.1) break;
if (absolutetime && totalTime[truncate] >= 0.1) break;
truncate--;
}
if (parameterValue[truncate] > maxX) maxX = parameterValue[truncate];
// we only do zone labels when using absolute values
refreshZoneLabels();
refreshHRZoneLabels();
} else { // ZONED
QFont labelFont;
labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
// 0.625 = golden ratio for gaps betwen group of cols
// 0.9 = 10% space between each col in group
double width = (0.625 / ncols) * 0.90f;
double jump = acol * (0.625 / ncols);
// we're not binning instead we are prettyfing the columnar
// display in much the same way as the weekly summary workds
// Each zone column will have 4 points
QVector<double> xaxis (array->size() * 4);
QVector<double> yaxis (array->size() * 4);
// so we can calculate percentage for the labels
double total=0;
for (int i=0; i<array->size(); i++) total += dt * (double)(*array)[i];
// samples to time
for (int i=0, offset=0; i<array->size(); i++) {
double x = double(i) - (0.625f * 0.90f / 2.0f);
double y = dt * (double)(*array)[i];
xaxis[offset] = x +jump;
yaxis[offset] = 0;
offset++;
xaxis[offset] = x +jump;
yaxis[offset] = y;
offset++;
xaxis[offset] = x +jump +width;
yaxis[offset] = y;
offset++;
xaxis[offset] = x +jump +width;
yaxis[offset] = 0;
offset++;
double yval = absolutetime ? y : (y /total * 100.00f);
if (yval > 0) {
QColor color = rangemode ? context->compareDateRanges[intervalNumber].color.darker(200)
: context->compareIntervals[intervalNumber].color.darker(200);
// now add a label above the bar
QwtPlotMarker *label = new QwtPlotMarker();
QwtText text(QString("%1%2").arg(int(yval)).arg(absolutetime ? "" : "%"), QwtText::PlainText);
text.setFont(labelFont);
text.setColor(color);
label->setLabel(text);
label->setValue(x+jump+(width/2.00f), yval);
label->setYAxis(QwtPlot::yLeft);
label->setSpacing(5); // not px but by yaxis value !? mad.
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
// and attach
label->attach(this);
zoneDataLabels << label;
}
}
if (!absolutetime) {
percentify(yaxis, 2);
}
// set those curves
curve->setPen(QPen(Qt::NoPen));
curve->setSamples(xaxis.data(), yaxis.data(), xaxis.size());
//
// POWER ZONES
//
if (cpzoned) {
setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
} else {
const Zones *zones;
int zone_range = -1;
zones = context->athlete->zones();
if (zones) {
if (context->compareIntervals.count())
zone_range = zones->whichRange(context->compareIntervals[0].data->startTime().date());
if (zone_range == -1) zone_range = zones->whichRange(QDate::currentDate());
}
if (zones && zone_range != -1) {
if ((series == RideFile::watts || series == RideFile::wattsKg)) {
setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(zones, zone_range));
setAxisScale(QwtPlot::xBottom, -0.99, zones->numZones(zone_range), 1);
}
}
}
//
// HR ZONES
//
const HrZones *hrzones;
int hrzone_range = -1;
hrzones = context->athlete->hrZones();
if (hrzones) {
if (context->compareIntervals.count())
hrzone_range = hrzones->whichRange(context->compareIntervals[0].data->startTime().date());
if (hrzone_range == -1) hrzone_range = hrzones->whichRange(QDate::currentDate());
}
if (hrzones && hrzone_range != -1) {
if (series == RideFile::hr) {
setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(hrzones, hrzone_range));
setAxisScale(QwtPlot::xBottom, -0.99, hrzones->numZones(hrzone_range), 1);
}
}
setAxisMaxMinor(QwtPlot::xBottom, 0);
// keep track of columns visible -- depending upon mode
if (!rangemode && context->compareIntervals[intervalNumber].isChecked()) acol++;
if (rangemode && context->compareDateRanges[intervalNumber].isChecked()) acol++;
}
}
// set axis etc
if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::wattsKg
&& series != RideFile::hr))) {
//normal
setAxisScale(xBottom, minX, maxX);
} else {
// zoned
}
setYMax();
updatePlot();
}
void
PowerHist::recalc(bool force)
{
if ((!rangemode && context->isCompareIntervals) ||
(rangemode && context->isCompareDateRanges)) {
recalcCompare();
return;
}
// lets make sure we need to recalculate
if (force == false &&
LASTsource == source &&
LASTcache == cache &&
LASTrideItem == rideItem &&
LASTseries == series &&
LASTshade == shade &&
LASTuseMetricUnits == context->athlete->useMetricUnits &&
LASTlny == lny &&
LASTzoned == zoned &&
LASTcpzoned == cpzoned &&
LASTbinw == binw &&
LASTwithz == withz &&
LASTdt == dt &&
LASTabsolutetime == absolutetime) {
return; // nothing has changed
} else {
// remember for next time
LASTsource = source;
LASTcache = cache;
LASTrideItem = rideItem;
LASTseries = series;
LASTshade = shade;
LASTuseMetricUnits = context->athlete->useMetricUnits;
LASTlny = lny;
LASTzoned = zoned;
LASTcpzoned = cpzoned;
LASTbinw = binw;
LASTwithz = withz;
LASTdt = dt;
LASTabsolutetime = absolutetime;
}
if (source == Ride && !rideItem) {
return;
}
// make sure the interval length is set if not plotting metrics
if (source != Metric && dt <= 0) {
return;
}
// zap any zone data labels
foreach (QwtPlotMarker *label, zoneDataLabels) {
label->detach();
delete label;
}
zoneDataLabels.clear();
// bin the data
QVector<double>x,y,sx,sy;
binData(standard, x, y, sx, sy);
if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::wattsKg && series != RideFile::hr))) {
// now draw curves / axis etc
curve->setSamples(x, y);
curveSelected->setSamples(sx, sy);
QwtScaleDraw *sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
setAxisScaleDraw(QwtPlot::xBottom, sd);
// HR typically starts at 80 or so, rather than zero
// lets crop the chart so we can focus on the data
// if we're working with HR data...
minX=0;
if (!withz && series == RideFile::hr) {
for (int i=1; i<standard.hrArray.size(); i++) {
if (standard.hrArray[i] > 0.1) {
minX = i;
break;
}
}
}
setAxisScale(xBottom, minX, x[x.size()-1]);
// we only do zone labels when using absolute values
refreshZoneLabels();
refreshHRZoneLabels();
} else {
QFont labelFont;
labelFont.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
labelFont.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 10).toInt());
// zoned labels
for(int i=1; i<x.count(); i += 4) {
double yval = y[i];
double xval = x[i];
if (yval > 0) {
// now add a label above the bar
QwtPlotMarker *label = new QwtPlotMarker();
QwtText text(QString("%1%2").arg(int(yval)).arg(absolutetime ? "" : "%"), QwtText::PlainText);
text.setFont(labelFont);
text.setColor(series == RideFile::watts ? GColor(CPOWER).darker(200) : GColor(CHEARTRATE).darker(200));
label->setLabel(text);
label->setValue(xval+0.312f, yval);
label->setYAxis(QwtPlot::yLeft);
label->setSpacing(5); // not px but by yaxis value !? mad.
label->setLabelAlignment(Qt::AlignTop | Qt::AlignCenter);
// and attach
label->attach(this);
zoneDataLabels << label;
}
}
// set those curves
curve->setSamples(x, y);
curveSelected->setSamples(sx, sy);
// zone scale draw
if ((series == RideFile::watts || series == RideFile::wattsKg) && zoned && rideItem && rideItem->zones) {
if (cpzoned) {
setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
} else {
setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(rideItem->zones, rideItem->zoneRange()));
if (rideItem->zoneRange() >= 0)
setAxisScale(QwtPlot::xBottom, -0.99, rideItem->zones->numZones(rideItem->zoneRange()), 1);
else
setAxisScale(QwtPlot::xBottom, -0.99, 0, 1);
}
}
// hr scale draw
int hrRange;
if (series == RideFile::hr && zoned && rideItem && context->athlete->hrZones() &&
(hrRange=context->athlete->hrZones()->whichRange(rideItem->dateTime.date())) != -1) {
setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(context->athlete->hrZones(), hrRange));
if (hrRange >= 0)
setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->hrZones()->numZones(hrRange), 1);
else
setAxisScale(QwtPlot::xBottom, -0.99, 0, 1);
}
// watts zoned for a time range
if (source == Cache && zoned && (series == RideFile::watts || series == RideFile::wattsKg) && context->athlete->zones()) {
if (cpzoned) {
setAxisScaleDraw(QwtPlot::xBottom, new PolarisedZoneScaleDraw());
setAxisScale(QwtPlot::xBottom, -0.99, 3, 1);
} else {
setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(context->athlete->zones(), 0));
if (context->athlete->zones()->getRangeSize())
setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->zones()->numZones(0), 1); // use zones from first defined range
}
}
// hr zoned for a time range
if (source == Cache && zoned && series == RideFile::hr && context->athlete->hrZones()) {
setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(context->athlete->hrZones(), 0));
if (context->athlete->hrZones()->getRangeSize())
setAxisScale(QwtPlot::xBottom, -0.99, context->athlete->hrZones()->numZones(0), 1); // use zones from first defined range
}
setAxisMaxMinor(QwtPlot::xBottom, 0);
}
setYMax();
configChanged(); // setup the curve colors to appropriate values
updatePlot();
}
// bin the data into x/y for ride and selected intervals
void
PowerHist::binData(HistData &standard, QVector<double>&x, // x-axis for data
QVector<double>&y, // y-axis for data
QVector<double>&sx, // x-axis for selected data
QVector<double>&sy) // y-axis for selected data
{
QVector<unsigned int> *array = NULL;
QVector<unsigned int> *selectedArray = NULL;
int arrayLength = 0;
if (source == Metric) {
// we use the metricArray
array = &standard.metricArray;
arrayLength = standard.metricArray.size();
selectedArray = NULL;
} else if (series == RideFile::watts && zoned == false) {
array = &standard.wattsArray;
arrayLength = standard.wattsArray.size();
selectedArray = &standard.wattsSelectedArray;
} else if ((series == RideFile::watts || series == RideFile::wattsKg) && zoned == true) {
if (cpzoned) {
array = &standard.wattsCPZoneArray;
arrayLength = standard.wattsCPZoneArray.size();
selectedArray = &standard.wattsCPZoneSelectedArray;
} else {
array = &standard.wattsZoneArray;
arrayLength = standard.wattsZoneArray.size();
selectedArray = &standard.wattsZoneSelectedArray;
}
} else if (series == RideFile::aPower && zoned == false) {
array = &standard.aPowerArray;
arrayLength = standard.aPowerArray.size();
selectedArray = &standard.aPowerSelectedArray;
} else if (series == RideFile::wattsKg && zoned == false) {
array = &standard.wattsKgArray;
arrayLength = standard.wattsKgArray.size();
selectedArray = &standard.wattsKgSelectedArray;
} else if (series == RideFile::nm) {
array = &standard.nmArray;
arrayLength = standard.nmArray.size();
selectedArray = &standard.nmSelectedArray;
} else if (series == RideFile::hr && zoned == false) {
array = &standard.hrArray;
arrayLength = standard.hrArray.size();
selectedArray = &standard.hrSelectedArray;
} else if (series == RideFile::hr && zoned == true) {
array = &standard.hrZoneArray;
arrayLength = standard.hrZoneArray.size();
selectedArray = &standard.hrZoneSelectedArray;
} else if (series == RideFile::kph) {
array = &standard.kphArray;
arrayLength = standard.kphArray.size();
selectedArray = &standard.kphSelectedArray;
} else if (series == RideFile::cad) {
array = &standard.cadArray;
arrayLength = standard.cadArray.size();
selectedArray = &standard.cadSelectedArray;
}
// binning of data when not zoned - we can't zone for series besides
// watts and hr so ignore zoning for those data series
if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::wattsKg && series != RideFile::hr))) {
// we add a bin on the end since the last "incomplete" bin
// will be dropped otherwise
int count = qMax(0, int(ceil((arrayLength - 1) / (binw)))+1);
// allocate space for data, plus beginning and ending point
x.resize(count+2);
y.resize(count+2);
sy.resize(count+2);
x.fill(0.0);
y.fill(0.0);
sx.fill(0.0);
int i;
for (i = 1; i <= count; ++i) {
double high = i * round(binw/delta);
double low = high - round(binw/delta);
if (low==0 && !withz) low++;
x[i] = high*delta;
y[i] = 1e-9; // nonzero to accomodate log plot
sy[i] = 1e-9; // nonzero to accomodate log plot
while (low < high && low<arrayLength) {
if (selectedArray && (*selectedArray).size()>low)
sy[i] += dt * (*selectedArray)[low];
y[i] += dt * (*array)[low++];
}
}
y[i] = 1e-9; // nonzero to accomodate log plot
sy[i] = 1e-9; // nonzero to accomodate log plot
x[i] = i * delta * binw;
y[0] = 1e-9;
sy[0] = 1e-9;
x[0] = 0;
// convert vectors from absolute time to percentage
// if the user has selected that
if (!absolutetime) {
percentify(y, 1);
percentify(sy, 1);
}
// the selected parameter values are the same as the
// actual values
sx = x;
} else {
// we're not binning instead we are prettyfing the columnar
// display in much the same way as the weekly summary workds
// Each zone column will have 4 points
x.resize(array->size() * 4);
y.resize(array->size() * 4);
sx.resize(selectedArray->size() * 4);
sy.resize(selectedArray->size() * 4);
// so we can calculate percentage for the labels
double total=0;
for (int i=0; i<array->size(); i++) total += dt * (double)(*array)[i];
// samples to time
for (int i=0, offset=0; i<array->size(); i++) {
double xn = (double) i - (0.625f / 2.0f);
double yn = dt * (double)(*array)[i];
x[offset] = xn;
y[offset] = 0;
offset++;
x[offset] = xn;
y[offset] = yn;
offset++;
x[offset] = xn+0.625;
y[offset] = yn;
offset++;
x[offset] = xn +0.625;
y[offset] = 0;
offset++;
}
for (int i=0, offset=0; i<selectedArray->size(); i++) {
double xn = (double)i - (0.625f / 2.0f);
double yn = dt * (double)(*selectedArray)[i];
sx[offset] = xn;
sy[offset] = 0;
offset++;
sx[offset] = xn;
sy[offset] = yn;
offset++;
sx[offset] = xn+0.625;
sy[offset] = yn;
offset++;
sx[offset] = xn +0.625;
sy[offset] = 0;
offset++;
}
if (!absolutetime) {
percentify(y, 2);
percentify(sy, 2);
}
}
}
void
PowerHist::setYMax()
{
double MaxY=0;
if (!rangemode && context->isCompareIntervals) {
int i=0;
foreach (QwtPlotCurve *p, compareCurves) {
// if its not visible don't set for it
if (context->compareIntervals[i].isChecked()) {
double my = p->maxYValue();
if (my > MaxY) MaxY = my;
}
i++;
}
} else if (rangemode && context->isCompareDateRanges) {
int i=0;
foreach (QwtPlotCurve *p, compareCurves) {
// if its not visible don't set for it
if (context->compareDateRanges[i].isChecked()) {
double my = p->maxYValue();
if (my > MaxY) MaxY = my;
}
i++;
}
} else {
MaxY = curve->maxYValue();
if (MaxY < curveSelected->maxYValue()) MaxY = curveSelected->maxYValue();
}
static const double tmin = 1.0/60;
setAxisScale(yLeft, (lny ? tmin : 0.0), MaxY * 1.1);
QwtScaleDraw *sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
sd->enableComponent(QwtScaleDraw::Ticks, false);
sd->enableComponent(QwtScaleDraw::Backbone, false);
setAxisScaleDraw(QwtPlot::yLeft, sd);
}
static void
longFromDouble(QVector<unsigned int>&here, QVector<double>&there)
{
int highest = 0;
here.resize(there.size());
for (int i=0; i<here.size(); i++) {
here[i] = there[i];
if (here[i] != 0) highest = i;
}
here.resize(highest);
}
void
PowerHist::setData(RideFileCache *cache)
{
source = Cache;
this->cache = cache;
dt = 1.0f / 60.0f; // rideFileCache is normalised to 1secs
// we set with this data already?
if (cache == LASTcache && source == LASTsource) return;
// Now go set all those tedious arrays from
// the ride cache
standard.wattsArray.resize(0);
standard.wattsZoneArray.resize(10);
standard.hrZoneArray.resize(10);
standard.wattsCPZoneArray.resize(3);
standard.wattsKgArray.resize(0);
standard.aPowerArray.resize(0);
standard.nmArray.resize(0);
standard.hrArray.resize(0);
standard.kphArray.resize(0);
standard.cadArray.resize(0);
// we do not use the selected array since it is
// not meaningful to overlay interval selection
// with long term data
standard.wattsSelectedArray.resize(0);
standard.wattsZoneSelectedArray.resize(0);
standard.wattsKgSelectedArray.resize(0);
standard.aPowerSelectedArray.resize(0);
standard.nmSelectedArray.resize(0);
standard.hrSelectedArray.resize(0);
standard.hrZoneSelectedArray.resize(0);
standard.kphSelectedArray.resize(0);
standard.cadSelectedArray.resize(0);
longFromDouble(standard.wattsArray, cache->distributionArray(RideFile::watts));
longFromDouble(standard.wattsKgArray, cache->distributionArray(RideFile::wattsKg));
longFromDouble(standard.aPowerArray, cache->distributionArray(RideFile::aPower));
longFromDouble(standard.hrArray, cache->distributionArray(RideFile::hr));
longFromDouble(standard.nmArray, cache->distributionArray(RideFile::nm));
longFromDouble(standard.cadArray, cache->distributionArray(RideFile::cad));
longFromDouble(standard.kphArray, cache->distributionArray(RideFile::kph));
if (!context->athlete->useMetricUnits) {
double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215);
double speed_factor = (context->athlete->useMetricUnits ? 1.0 : 0.62137119);
for(int i=0; i<standard.nmArray.size(); i++) standard.nmArray[i] = standard.nmArray[i] * torque_factor;
for(int i=0; i<standard.kphArray.size(); i++) standard.kphArray[i] = standard.kphArray[i] * speed_factor;
}
// zone array
for (int i=0; i<10; i++) {
standard.wattsZoneArray[i] = cache->wattsZoneArray()[i];
standard.hrZoneArray[i] = cache->hrZoneArray()[i];
}
// polarised zones
standard.wattsCPZoneArray[0] = cache->wattsCPZoneArray()[1];
if (withz) {
standard.wattsCPZoneArray[0] += cache->wattsCPZoneArray()[0]; // add in zero watts
}
standard.wattsCPZoneArray[1] = cache->wattsCPZoneArray()[2];
standard.wattsCPZoneArray[2] = cache->wattsCPZoneArray()[3];
curveSelected->hide();
curveHover->hide();
}
void
PowerHist::intervalHover(RideFileInterval x)
{
// telling me to hide
if (x.start == 0 && x.stop == 0) {
curveHover->hide();
return;
}
if (!isVisible()) return; // noone can see us
if ((rangemode && context->isCompareDateRanges)
|| (!rangemode && context->isCompareIntervals)) return; // not in compare mode
if (rideItem && rideItem->ride()) {
// set data
HistData hoverData;
setArraysFromRide(rideItem->ride(), hoverData, rideItem->zones, x);
// set curve
QVector<double>x,y,sx,sy;
binData(hoverData, x,y,sx,sy);
curveHover->setSamples(sx,sy);
curveHover->show();
replot();
}
}
void
PowerHist::setDataFromCompare()
{
// not for metric plots sonny
if (source == Metric) return;
double width = appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble();
// set all the curves based upon whats in the compare intervals array
// first lets clear the old data
compareData.clear();
// and remove the old curves
foreach(QwtPlotCurve *x, compareCurves) {
x->detach();
delete x;
}
compareCurves.clear();
// now lets setup a HistData for each CompareInterval
for(int intervalNumber=0; (rangemode && intervalNumber < context->compareDateRanges.count()) ||
(!rangemode && intervalNumber < context->compareIntervals.count()) ; intervalNumber++) {
// get the bits we need from either the interval or daterange compare object
RideFileCache *s = rangemode ? context->compareDateRanges[intervalNumber].rideFileCache() :
context->compareIntervals[intervalNumber].rideFileCache();
QColor color = rangemode ? context->compareDateRanges[intervalNumber].color :
context->compareIntervals[intervalNumber].color;
bool ischecked = rangemode ? context->compareDateRanges[intervalNumber].isChecked() :
context->compareIntervals[intervalNumber].isChecked();
// set the data even if not checked
HistData add;
// Now go set all those tedious arrays from
// the ride cache
add.wattsArray.resize(0);
add.wattsZoneArray.resize(10);
add.wattsCPZoneArray.resize(3);
add.wattsKgArray.resize(0);
add.aPowerArray.resize(0);
add.nmArray.resize(0);
add.hrArray.resize(0);
add.hrZoneArray.resize(10);
add.kphArray.resize(0);
add.cadArray.resize(0);
longFromDouble(add.wattsArray, s->distributionArray(RideFile::watts));
longFromDouble(add.wattsKgArray, s->distributionArray(RideFile::wattsKg));
longFromDouble(add.aPowerArray, s->distributionArray(RideFile::aPower));
longFromDouble(add.hrArray, s->distributionArray(RideFile::hr));
longFromDouble(add.nmArray, s->distributionArray(RideFile::nm));
longFromDouble(add.cadArray, s->distributionArray(RideFile::cad));
longFromDouble(add.kphArray, s->distributionArray(RideFile::kph));
// convert for metric imperial types
if (!context->athlete->useMetricUnits) {
double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215);
double speed_factor = (context->athlete->useMetricUnits ? 1.0 : 0.62137119);
for(int i=0; i<add.nmArray.size(); i++) add.nmArray[i] = add.nmArray[i] * torque_factor;
for(int i=0; i<add.kphArray.size(); i++) add.kphArray[i] = add.kphArray[i] * speed_factor;
}
// zone array
for (int i=0; i<10; i++) {
add.wattsZoneArray[i] = s->wattsZoneArray()[i];
add.hrZoneArray[i] = s->hrZoneArray()[i];
}
// polarised zones
add.wattsCPZoneArray[0] = s->wattsCPZoneArray()[1];
if (withz) {
add.wattsCPZoneArray[0] += s->wattsCPZoneArray()[0]; // add in zero watts
}
add.wattsCPZoneArray[1] = s->wattsCPZoneArray()[2];
add.wattsCPZoneArray[2] = s->wattsCPZoneArray()[3];
// add to the list
compareData << add;
// now add a curve for recalc to play with
QwtPlotCurve *newCurve = new QwtPlotCurve("");
newCurve->setStyle(QwtPlotCurve::Steps);
if (appsettings->value(this, GC_ANTIALIAS, false).toBool()==true)
newCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
// curve has no brush .. too confusing...
QPen pen;
pen.setColor(color);
pen.setWidth(width);
newCurve->setPen(pen);
QColor brush_color = color;
brush_color.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 120 : 200);
QColor brush_color1 = brush_color.darker();
//QLinearGradient linearGradient(0, 0, 0, height());
//linearGradient.setColorAt(0.0, brush_color);
//linearGradient.setColorAt(1.0, brush_color1);
//linearGradient.setSpread(QGradient::PadSpread);
newCurve->setBrush(brush_color1); // fill below the line
// hide and show, but always attach
newCurve->setVisible(ischecked);
newCurve->attach(this);
// we do want a legend in compare mode
//XXX newCurve->setItemAttribute(QwtPlotItem::Legend, true);
// add to our collection
compareCurves << newCurve;
}
// show legend in compare mode
//XXX legend()->show();
//XXX updateLegend();
}
void
PowerHist::setComparePens()
{
// no compare? don't bother
if ((!rangemode && !context->isCompareIntervals) ||
(rangemode && !context->isCompareDateRanges)) return;
double width = appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble();
for (int i=0; (!rangemode && i<context->compareIntervals.count()) ||
(rangemode && i<context->compareDateRanges.count()); i++) {
if (zoned == false || (zoned == true && (series != RideFile::watts && series != RideFile::wattsKg
&& series != RideFile::hr))) {
// NOT ZONED
if (compareCurves.count() > i) {
// set pen back
QPen pen;
pen.setColor(rangemode ? context->compareDateRanges[i].color :
context->compareIntervals[i].color);
pen.setWidth(width);
compareCurves[i]->setPen(pen);
}
} else {
if (compareCurves.count() > i) {
// set pen back
compareCurves[i]->setPen(QPen(Qt::NoPen));
}
}
}
}
// set the metric arrays up for each date range using the
// summary metrics in the Context::CompareDateRange array
void
PowerHist::setDataFromCompare(QString totalMetric, QString distMetric)
{
// set the data for each compare date range using
// the metric results in the compare data range
// and create the HistData and curves associated
double width = appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble();
// remove old data and curves
compareData.clear();
foreach(QwtPlotCurve *x, compareCurves) {
x->detach();
delete x;
}
compareCurves.clear();
// now lets setup a HistData for each CompareDate Range
foreach(CompareDateRange cd, context->compareDateRanges) {
// set the data even if not checked
HistData add;
// set it from the metric
setData(cd.metrics, totalMetric, distMetric, false, QStringList(), &add);
// add to the list
compareData << add;
// now add a curve for recalc to play with
QwtPlotCurve *newCurve = new QwtPlotCurve("");
newCurve->setStyle(QwtPlotCurve::Steps);
if (appsettings->value(this, GC_ANTIALIAS, false).toBool()==true)
newCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
// curve has no brush .. too confusing...
QPen pen;
pen.setColor(cd.color);
pen.setWidth(width);
newCurve->setPen(pen);
QColor brush_color = cd.color;
brush_color.setAlpha(GColor(CPLOTBACKGROUND) == QColor(Qt::white) ? 120 : 200);
QColor brush_color1 = brush_color.darker();
//QLinearGradient linearGradient(0, 0, 0, height());
//linearGradient.setColorAt(0.0, brush_color);
//linearGradient.setColorAt(1.0, brush_color1);
//linearGradient.setSpread(QGradient::PadSpread);
newCurve->setBrush(brush_color1); // fill below the line
// hide and show, but always attach
newCurve->setVisible(cd.isChecked());
newCurve->attach(this);
// we do want a legend in compare mode
//XXX newCurve->setItemAttribute(QwtPlotItem::Legend, true);
// add to our collection
compareCurves << newCurve;
}
}
void
PowerHist::setData(QList<SummaryMetrics>&results, QString totalMetric, QString distMetric,
bool isFiltered, QStringList files, HistData *data)
{
// what metrics are we plotting?
source = Metric;
const RideMetricFactory &factory = RideMetricFactory::instance();
const RideMetric *m = factory.rideMetric(distMetric);
const RideMetric *tm = factory.rideMetric(totalMetric);
if (m == NULL || tm == NULL) return;
// metricX, metricY
metricX = distMetric;
metricY = totalMetric;
// how big should the array be?
double multiplier = pow(10, m->precision());
double max = 0, min = 0;
// LOOP THRU VALUES -- REPEATED WITH CUT AND PASTE BELOW
// SO PLEASE MAKE SAME CHANGES TWICE (SORRY)
foreach(SummaryMetrics x, results) {
// skip filtered values
if (isFiltered && !files.contains(x.getFileName())) continue;
// and global filter too
if (context->isfiltered && !context->filters.contains(x.getFileName())) continue;
// get computed value
double v = x.getForSymbol(distMetric, context->athlete->useMetricUnits);
// ignore no temp files
if ((distMetric == "average_temp" || distMetric == "max_temp") && v == RideFile::noTemp) continue;
// clean up dodgy values
if (isnan(v) || isinf(v)) v = 0;
// seconds to minutes
if (m->units(context->athlete->useMetricUnits) == "seconds" ||
m->units(context->athlete->useMetricUnits) == tr("seconds")) v /= 60;
// apply multiplier
v *= multiplier;
if (v>max) max = v;
if (v<min) min = v;
}
// lets truncate the data if there are very high
// or very low max/min values, to ensure we don't exhaust memory
if (max > 100000) max = 100000;
if (min < -100000) min = -100000;
// now run thru the data again, but this time
// populate the metricArray
// we add 1 to account for possible rounding up
data->metricArray.resize(1 + (int)(max)-(int)(min));
data->metricArray.fill(0);
// LOOP THRU VALUES -- REPEATED WITH CUT AND PASTE ABOVE
// SO PLEASE MAKE SAME CHANGES TWICE (SORRY)
foreach(SummaryMetrics x, results) {
// skip filtered values
if (isFiltered && !files.contains(x.getFileName())) continue;
// and global filter too
if (context->isfiltered && !context->filters.contains(x.getFileName())) continue;
// get computed value
double v = x.getForSymbol(distMetric, context->athlete->useMetricUnits);
// ignore no temp files
if ((distMetric == "average_temp" || distMetric == "max_temp") && v == RideFile::noTemp) continue;
// clean up dodgy values
if (isnan(v) || isinf(v)) v = 0;
// seconds to minutes
if (m->units(context->athlete->useMetricUnits) == "seconds" ||
m->units(context->athlete->useMetricUnits) == tr("seconds")) v /= 60;
// apply multiplier
v *= multiplier;
// ignore out of bounds data
if ((int)(v)<min || (int)(v)>max) continue;
// increment value, are intitialised to zero above
// there will be some loss of precision due to totalising
// a double in an int, but frankly that should be minimal
// since most values of note are integer based anyway.
double t = x.getForSymbol(totalMetric, context->athlete->useMetricUnits);
// totalise in minutes
if (tm->units(context->athlete->useMetricUnits) == "seconds" ||
tm->units(context->athlete->useMetricUnits) == tr("seconds")) t /= 60;
// sum up
data->metricArray[(int)(v)-min] += t;
}
// we certainly don't want the interval curve when plotting
// metrics across rides!
curveSelected->hide();
// now set all the plot paramaters to match the data
source = Metric;
zoned = false;
rideItem = NULL;
lny = false;
shade = false;
withz = false;
dt = 1;
absolutetime = true;
// and the plot itself
QString yunits = tm->units(context->athlete->useMetricUnits);
if (yunits == "seconds" || yunits == tr("seconds")) yunits = tr("minutes");
QString xunits = m->units(context->athlete->useMetricUnits);
if (xunits == "seconds" || xunits == tr("seconds")) xunits = tr("minutes");
if (tm->units(context->athlete->useMetricUnits) != "")
setAxisTitle(yLeft, QString(tr("Total %1 (%2)")).arg(tm->name()).arg(yunits));
else
setAxisTitle(yLeft, QString(tr("Total %1")).arg(tm->name()));
if (m->units(context->athlete->useMetricUnits) != "")
setAxisTitle(xBottom, QString(tr("%1 of Ride (%2)")).arg(m->name()).arg(xunits));
else
setAxisTitle(xBottom, QString(tr("%1 of Ride")).arg(m->name()));
// dont show legend in metric mode
//XXX legend()->hide();
//XXX updateLegend();
}
void
PowerHist::updatePlot()
{
replot();
zoomer->setZoomBase();
}
void
PowerHist::setData(RideItem *_rideItem, bool force)
{
source = Ride;
// hide hover curve just in case its there
// from previous ride
curveHover->hide();
// we set with this data already
if (force == false && _rideItem == LASTrideItem && source == LASTsource) return;
rideItem = _rideItem;
if (!rideItem) return;
RideFile *ride = rideItem->ride();
bool hasData = ((series == RideFile::watts || series == RideFile::wattsKg) && ride->areDataPresent()->watts) ||
(series == RideFile::nm && ride->areDataPresent()->nm) ||
(series == RideFile::kph && ride->areDataPresent()->kph) ||
(series == RideFile::cad && ride->areDataPresent()->cad) ||
(series == RideFile::aPower && ride->areDataPresent()->apower) ||
(series == RideFile::hr && ride->areDataPresent()->hr);
if (ride && hasData) {
//setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
setArraysFromRide(ride, standard, rideItem->zones, RideFileInterval());
} else {
// create empty curves when no data
const double zero = 0;
curve->setSamples(&zero, &zero, 0);
curveSelected->setSamples(&zero, &zero, 0);
updatePlot();
}
curveSelected->show();
zoomer->setZoomBase();
// dont show legend in metric mode
//XXX legend()->hide();
//XXX updateLegend();
}
void
PowerHist::setArraysFromRide(RideFile *ride, HistData &standard, const Zones *zones, RideFileInterval hover)
{
// predefined deltas for each series
static const double wattsDelta = 1.0;
static const double wattsKgDelta = 0.01;
static const double nmDelta = 0.1;
static const double hrDelta = 1.0;
static const double kphDelta = 0.1;
static const double cadDelta = 1.0;
static const int maxSize = 4096;
// recording interval in minutes
dt = ride->recIntSecs() / 60.0;
standard.wattsArray.resize(0);
standard.wattsZoneArray.resize(0);
standard.wattsCPZoneArray.resize(0);
standard.wattsKgArray.resize(0);
standard.aPowerArray.resize(0);
standard.nmArray.resize(0);
standard.hrArray.resize(0);
standard.hrZoneArray.resize(0);
standard.kphArray.resize(0);
standard.cadArray.resize(0);
standard.wattsSelectedArray.resize(0);
standard.wattsZoneSelectedArray.resize(0);
standard.wattsKgSelectedArray.resize(0);
standard.aPowerSelectedArray.resize(0);
standard.nmSelectedArray.resize(0);
standard.hrSelectedArray.resize(0);
standard.hrZoneSelectedArray.resize(0);
standard.kphSelectedArray.resize(0);
standard.cadSelectedArray.resize(0);
// unit conversion factor for imperial units for selected parameters
double torque_factor = (context->athlete->useMetricUnits ? 1.0 : 0.73756215);
double speed_factor = (context->athlete->useMetricUnits ? 1.0 : 0.62137119);
// cp and zones
int CP = 0;
int zoneRange = zones ? zones->whichRange(ride->startTime().date()) : -1;
if (zoneRange != -1) CP=zones->getCP(zoneRange);
foreach(const RideFilePoint *p1, ride->dataPoints()) {
// selected if hovered -or- selected depending on
// whether we were passed a blank or real RideFileInterval
bool selected = false;
if (hover.start != 0 && hover.stop != 0) {
if (p1->secs >= hover.start && p1->secs <= hover.stop) { selected = true; }
} else {
selected = isSelected(p1, ride->recIntSecs());
}
// watts array
int wattsIndex = int(floor(p1->watts / wattsDelta));
if (wattsIndex >= 0 && wattsIndex < maxSize) {
if (wattsIndex >= standard.wattsArray.size())
standard.wattsArray.resize(wattsIndex + 1);
standard.wattsArray[wattsIndex]++;
if (selected) {
if (wattsIndex >= standard.wattsSelectedArray.size())
standard.wattsSelectedArray.resize(wattsIndex + 1);
standard.wattsSelectedArray[wattsIndex]++;
}
}
// watts zoned array
// Only calculate zones if we have a valid range and check zeroes
if (zoneRange > -1 && (withz || (!withz && p1->watts))) {
// cp zoned
if (standard.wattsCPZoneArray.size() < 3) {
standard.wattsCPZoneArray.resize(3);
}
if (p1->watts < 1 && withz) { // moderate zero watts
standard.wattsCPZoneArray[0] ++;
} else if (p1->watts < (CP * 0.85f)) { // moderate
standard.wattsCPZoneArray[0] ++;
} else if (p1->watts < CP) { // heavy
standard.wattsCPZoneArray[1] ++;
} else { // severe
standard.wattsCPZoneArray[2] ++;
}
// get the zone
wattsIndex = zones->whichZone(zoneRange, p1->watts);
// zoned
if (wattsIndex >= 0 && wattsIndex < maxSize) {
if (wattsIndex >= standard.wattsZoneArray.size())
standard.wattsZoneArray.resize(wattsIndex + 1);
standard.wattsZoneArray[wattsIndex]++;
if (selected) {
if (wattsIndex >= standard.wattsZoneSelectedArray.size())
standard.wattsZoneSelectedArray.resize(wattsIndex + 1);
standard.wattsZoneSelectedArray[wattsIndex]++;
}
}
}
// aPower array
int aPowerIndex = int(floor(p1->apower / wattsDelta));
if (aPowerIndex >= 0 && aPowerIndex < maxSize) {
if (aPowerIndex >= standard.aPowerArray.size())
standard.aPowerArray.resize(aPowerIndex + 1);
standard.aPowerArray[aPowerIndex]++;
if (selected) {
if (aPowerIndex >= standard.aPowerSelectedArray.size())
standard.aPowerSelectedArray.resize(aPowerIndex + 1);
standard.aPowerSelectedArray[aPowerIndex]++;
}
}
// wattsKg array
int wattsKgIndex = int(floor(p1->watts / ride->getWeight() / wattsKgDelta));
if (wattsKgIndex >= 0 && wattsKgIndex < maxSize) {
if (wattsKgIndex >= standard.wattsKgArray.size())
standard.wattsKgArray.resize(wattsKgIndex + 1);
standard.wattsKgArray[wattsKgIndex]++;
if (selected) {
if (wattsKgIndex >= standard.wattsKgSelectedArray.size())
standard.wattsKgSelectedArray.resize(wattsKgIndex + 1);
standard.wattsKgSelectedArray[wattsKgIndex]++;
}
}
int nmIndex = int(floor(p1->nm * torque_factor / nmDelta));
if (nmIndex >= 0 && nmIndex < maxSize) {
if (nmIndex >= standard.nmArray.size())
standard.nmArray.resize(nmIndex + 1);
standard.nmArray[nmIndex]++;
if (selected) {
if (nmIndex >= standard.nmSelectedArray.size())
standard.nmSelectedArray.resize(nmIndex + 1);
standard.nmSelectedArray[nmIndex]++;
}
}
int hrIndex = int(floor(p1->hr / hrDelta));
if (hrIndex >= 0 && hrIndex < maxSize) {
if (hrIndex >= standard.hrArray.size())
standard.hrArray.resize(hrIndex + 1);
standard.hrArray[hrIndex]++;
if (selected) {
if (hrIndex >= standard.hrSelectedArray.size())
standard.hrSelectedArray.resize(hrIndex + 1);
standard.hrSelectedArray[hrIndex]++;
}
}
// hr zoned array
int hrZoneRange = context->athlete->hrZones() ? context->athlete->hrZones()->whichRange(ride->startTime().date()) : -1;
// Only calculate zones if we have a valid range
if (hrZoneRange > -1 && (withz || (!withz && p1->hr))) {
hrIndex = context->athlete->hrZones()->whichZone(hrZoneRange, p1->hr);
if (hrIndex >= 0 && hrIndex < maxSize) {
if (hrIndex >= standard.hrZoneArray.size())
standard.hrZoneArray.resize(hrIndex + 1);
standard.hrZoneArray[hrIndex]++;
if (selected) {
if (hrIndex >= standard.hrZoneSelectedArray.size())
standard.hrZoneSelectedArray.resize(hrIndex + 1);
standard.hrZoneSelectedArray[hrIndex]++;
}
}
}
int kphIndex = int(floor(p1->kph * speed_factor / kphDelta));
if (kphIndex >= 0 && kphIndex < maxSize) {
if (kphIndex >= standard.kphArray.size())
standard.kphArray.resize(kphIndex + 1);
standard.kphArray[kphIndex]++;
if (selected) {
if (kphIndex >= standard.kphSelectedArray.size())
standard.kphSelectedArray.resize(kphIndex + 1);
standard.kphSelectedArray[kphIndex]++;
}
}
int cadIndex = int(floor(p1->cad / cadDelta));
if (cadIndex >= 0 && cadIndex < maxSize) {
if (cadIndex >= standard.cadArray.size())
standard.cadArray.resize(cadIndex + 1);
standard.cadArray[cadIndex]++;
if (selected) {
if (cadIndex >= standard.cadSelectedArray.size())
standard.cadSelectedArray.resize(cadIndex + 1);
standard.cadSelectedArray[cadIndex]++;
}
}
}
}
void
PowerHist::setBinWidth(double value)
{
if (!value) value = 1; // binwidth must be nonzero
binw = value;
// hide the hover curve as it will now be wrong
if (!rangemode) curveHover->hide();
}
void
PowerHist::setCPZoned(bool value)
{
cpzoned = value;
setComparePens();
}
void
PowerHist::setZoned(bool value)
{
zoned = value;
setComparePens();
}
void
PowerHist::setWithZeros(bool value)
{
withz = value;
}
void
PowerHist::setlnY(bool value)
{
// note: setAxisScaleEngine deletes the old ScaleEngine, so specifying
// "new" in the argument list is not a leak
lny=value;
if (lny && !zoned) {
setAxisScaleEngine(yLeft, new QwtLogScaleEngine);
curve->setBaseline(1e-6);
curveSelected->setBaseline(1e-6);
} else {
setAxisScaleEngine(yLeft, new QwtLinearScaleEngine);
curve->setBaseline(0);
curveSelected->setBaseline(0);
}
setYMax();
updatePlot();
}
void
PowerHist::setSumY(bool value)
{
absolutetime = value;
setParameterAxisTitle();
}
void
PowerHist::setParameterAxisTitle()
{
QString axislabel;
switch (series) {
case RideFile::watts:
if (zoned) axislabel = tr("Power zone");
else axislabel = tr("Power (watts)");
break;
case RideFile::wattsKg:
if (zoned) axislabel = tr("Power zone");
else axislabel = tr("Power (watts/kg)");
break;
case RideFile::hr:
if (zoned) axislabel = tr("Heartrate zone");
else axislabel = tr("Heartrate (bpm)");
break;
case RideFile::aPower:
axislabel = tr("aPower (watts)");
break;
case RideFile::cad:
axislabel = tr("Cadence (rpm)");
break;
case RideFile::kph:
axislabel = QString(tr("Speed (%1)")).arg(context->athlete->useMetricUnits ? tr("kph") : tr("mph"));
break;
case RideFile::nm:
axislabel = QString(tr("Torque (%1)")).arg(context->athlete->useMetricUnits ? tr("N-m") : tr("ft-lbf"));
break;
default:
axislabel = QString(tr("Unknown data series"));
break;
}
setAxisTitle(xBottom, axislabel);
setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)"));
}
void
PowerHist::setAxisTitle(int axis, QString label)
{
// setup the default fonts
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
QwtText title(label);
title.setFont(stGiles);
QwtPlot::setAxisFont(axis, stGiles);
QwtPlot::setAxisTitle(axis, title);
}
void
PowerHist::setSeries(RideFile::SeriesType x)
{
// user selected a different series to plot
series = x;
configChanged(); // set colors
setParameterAxisTitle();
}
bool PowerHist::shadeZones() const
{
return (rideItem && rideItem->ride() && (series == RideFile::aPower || series == RideFile::watts || series == RideFile::wattsKg) && !zoned && shade == true);
}
bool PowerHist::shadeHRZones() const
{
return (rideItem && rideItem->ride() && series == RideFile::hr && !zoned && shade == true);
}
bool PowerHist::isSelected(const RideFilePoint *p, double sample) {
if (context->athlete->allIntervalItems() != NULL) {
for (int i=0; i<context->athlete->allIntervalItems()->childCount(); i++) {
IntervalItem *current = dynamic_cast<IntervalItem*>(context->athlete->allIntervalItems()->child(i));
if (current != NULL) {
if (current->isSelected() && p->secs+sample>current->start && p->secs<current->stop) {
return true;
}
}
}
}
return false;
}
void
PowerHist::pointHover(QwtPlotCurve *curve, int index)
{
if (index >= 0) {
double xvalue = curve->sample(index).x();
double yvalue = curve->sample(index).y();
QString text;
if (zoned && yvalue > 0) {
// output the tooltip
text = QString("%1 %2").arg(yvalue, 0, 'f', 1).arg(absolutetime ? tr("minutes") : tr("%"));
// set that text up
zoomer->setText(text);
return;
} else if (yvalue > 0) {
if (source != Metric) {
// output the tooltip
text = QString("%1 %2\n%3 %4")
.arg(xvalue, 0, 'f', digits)
.arg(this->axisTitle(curve->xAxis()).text())
.arg(yvalue, 0, 'f', 1)
.arg(absolutetime ? tr("minutes") : tr("%"));
} else {
text = QString("%1 %2\n%3 %4")
.arg(xvalue, 0, 'f', digits)
.arg(this->axisTitle(curve->xAxis()).text())
.arg(yvalue, 0, 'f', 1)
.arg(this->axisTitle(curve->yAxis()).text());
}
// set that text up
zoomer->setText(text);
return;
}
}
// no point
zoomer->setText("");
}
// because we need to effectively draw bars when showing
// time in zone (i.e. for every zone there are 2 points for each
// zone - top left and top right) we need to multiply the percentage
// values by 2 to take this into account
void
PowerHist::percentify(QVector<double> &array, double factor)
{
double total=0;
foreach (double current, array) total += current;
if (total > 0)
for (int i=0; i< array.size(); i++)
if (array[i] > 0.01) // greater than 0.8s (i.e. not a double storage issue)
array[i] = factor * (array[i] / total) * (double)100.00;
}