Files
GoldenCheetah/src/PowerHist.cpp
Mark Liversedge c1a8945a11 Histogram plot by zone for seasons
The recent update to plot histograms for seasons or other
date ranges did not support displaying by zone since the
cache did not contain zoned data. This patch fixes that
with an update to RideFileCache to pre-compute and to the
PowerHist class to retrieve and plot.

There are some minor issues that need to be addressed:

* Handling aggregation with different zone schemes

* Deciding which zone scheme to use for the bar labels
  when multiple differing schemes have been used within
  the date range selected.

* Showing a break down of time in zone by range i.e.
  how much time was spent at Threshold when CP was X
  as opposed to when it was Y (hint: do it like we
  currently display intervals when plotting a single
  ride).

* Refreshing the Time In Zone data in the .cpx file
  when CP/LTHR changes is not implemented.

The RideFileCache now checks the version of the cache to
determine if it needs to be refreshed -- so no need to
delete old .cpx files before running GC with this patch.
2011-05-03 16:26:40 +01:00

1000 lines
30 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 "RideItem.h"
#include "IntervalItem.h"
#include "RideFile.h"
#include "RideFileCache.h"
#include "Settings.h"
#include "Zones.h"
#include "HrZones.h"
#include "Colors.h"
#include "ZoneScaleDraw.h"
#include <assert.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_text.h>
#include <qwt_legend.h>
#include <qwt_data.h>
#include "LTMCanvasPicker.h" // for tooltip
PowerHist::PowerHist(MainWindow *mainWindow):
rideItem(NULL),
mainWindow(mainWindow),
series(RideFile::watts),
useMetricUnits(true),
unit(0),
lny(false),
shade(false),
zoned(false),
binw(3),
withz(true),
dt(1),
absolutetime(true),
cache(NULL),
source(Ride)
{
unit = appsettings->value(this, GC_UNIT);
useMetricUnits = (unit.toString() == "Metric");
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);
setParameterAxisTitle();
setAxisTitle(yLeft, absolutetime ? tr("Time (minutes)") : tr("Time (percent)"));
curve = new QwtPlotCurve("");
curve->setStyle(QwtPlotCurve::Steps);
curve->setRenderHint(QwtPlotItem::RenderAntialiased);
curve->attach(this);
curveSelected = new QwtPlotCurve("");
curveSelected->setStyle(QwtPlotCurve::Steps);
curveSelected->setRenderHint(QwtPlotItem::RenderAntialiased);
curveSelected->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)));
configChanged();
}
void
PowerHist::configChanged()
{
// plot background
setCanvasBackground(GColor(CPLOTBACKGROUND));
// curve
QPen pen;
QColor brush_color;
switch (series) {
case RideFile::watts:
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);
}
pen.setWidth(width);
curve->setPen(pen);
brush_color.setAlpha(64);
curve->setBrush(brush_color); // fill below the line
// intervalselection
QPen ivl(GColor(CINTERVALHIGHLIGHTER).darker(200));
ivl.setWidth(width);
curveSelected->setPen(ivl);
QColor ivlbrush = GColor(CINTERVALHIGHLIGHTER);
ivlbrush.setAlpha(64);
curveSelected->setBrush(ivlbrush); // fill below the line
// grid
QPen gridPen(GColor(CPLOTGRID));
gridPen.setStyle(Qt::DotLine);
grid->setPen(gridPen);
}
PowerHist::~PowerHist() {
delete bg;
delete hrbg;
delete curve;
delete curveSelected;
delete grid;
}
// static const variables from PoweHist.h:
// discritized unit for smoothing
const double PowerHist::wattsDelta;
const double PowerHist::nmDelta;
const double PowerHist::hrDelta;
const double PowerHist::kphDelta;
const double PowerHist::cadDelta;
// digits for text entry validator
const int PowerHist::wattsDigits;
const int PowerHist::nmDigits;
const int PowerHist::hrDigits;
const int PowerHist::kphDigits;
const int PowerHist::cadDigits;
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) {
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 = mainWindow->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::recalc(bool force)
{
QVector<unsigned int> *array;
QVector<unsigned int> *selectedArray;
int arrayLength = 0;
double delta;
// lets make sure we need to recalculate
if (force == false &&
LASTsource == source &&
LASTcache == cache &&
LASTrideItem == rideItem &&
LASTseries == series &&
LASTshade == shade &&
LASTuseMetricUnits == useMetricUnits &&
LASTlny == lny &&
LASTzoned == zoned &&
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 = useMetricUnits;
LASTlny = lny;
LASTzoned = zoned;
LASTbinw = binw;
LASTwithz = withz;
LASTdt = dt;
LASTabsolutetime = absolutetime;
}
if (source == Ride && !rideItem) return;
// make sure the interval length is set
if (dt <= 0) return;
if (series == RideFile::watts && zoned == false) {
array = &wattsArray;
delta = wattsDelta;
arrayLength = wattsArray.size();
selectedArray = &wattsSelectedArray;
} else if (series == RideFile::watts && zoned == true) {
array = &wattsZoneArray;
delta = 1;
arrayLength = wattsZoneArray.size();
selectedArray = &wattsZoneSelectedArray;
} else if (series == RideFile::nm) {
array = &nmArray;
delta = nmDelta;
arrayLength = nmArray.size();
selectedArray = &nmSelectedArray;
} else if (series == RideFile::hr && zoned == false) {
array = &hrArray;
delta = hrDelta;
arrayLength = hrArray.size();
selectedArray = &hrSelectedArray;
} else if (series == RideFile::hr && zoned == true) {
array = &hrZoneArray;
delta = 1;
arrayLength = hrZoneArray.size();
selectedArray = &hrZoneSelectedArray;
} else if (series == RideFile::kph) {
array = &kphArray;
delta = kphDelta;
arrayLength = kphArray.size();
selectedArray = &kphSelectedArray;
} else if (series == RideFile::cad) {
array = &cadArray;
delta = cadDelta;
arrayLength = cadArray.size();
selectedArray = &cadSelectedArray;
}
// null curve please -- we have no data!
if (!array || arrayLength == 0 || (source == Ride && !rideItem->ride()->isDataPresent(series))) {
// create empty curves when no data
const double zero = 0;
curve->setData(&zero, &zero, 0);
curveSelected->setData(&zero, &zero, 0);
replot();
return;
}
// 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::hr))) {
// 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);
QVector<double> totalTimeSelected(count+2, 0.0);
int i;
for (i = 1; i <= count; ++i) {
int high = i * binw;
int low = high - binw;
if (low==0 && !withz) low++;
parameterValue[i] = high * delta;
totalTime[i] = 1e-9; // nonzero to accomodate log plot
totalTimeSelected[i] = 1e-9; // nonzero to accomodate log plot
while (low < high && low<arrayLength) {
if (selectedArray && (*selectedArray).size()>low)
totalTimeSelected[i] += dt * (*selectedArray)[low];
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);
percentify(totalTimeSelected, 1);
}
curve->setData(parameterValue.data(), totalTime.data(), count + 2);
curveSelected->setData(parameterValue.data(), totalTimeSelected.data(), count + 2);
// make see through if we're shading zones
QBrush brush = curve->brush();
QColor bcol = brush.color();
bool zoning = (zoned && (series == RideFile::watts || series == RideFile::hr));
bcol.setAlpha(zoning ? 165 : 200);
brush.setColor(bcol);
//curve->setBrush(brush); //XXX weird artefact on first run only?
setAxisScaleDraw(QwtPlot::xBottom, new QwtScaleDraw);
// 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...
if (series == RideFile::hr) {
double MinX=0;
for (int i=0; i<hrArray.size(); i++) {
if (hrArray[i] > 0) {
MinX = i;
break;
}
}
setAxisScale(xBottom, MinX, parameterValue[count + 1]);
} else {
setAxisScale(xBottom, 0.0, parameterValue[count + 1]);
}
// we only do zone labels when using absolute values
refreshZoneLabels();
refreshHRZoneLabels();
} 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
QVector<double> xaxis (array->size() * 4);
QVector<double> yaxis (array->size() * 4);
QVector<double> selectedxaxis (selectedArray->size() * 4);
QVector<double> selectedyaxis (selectedArray->size() * 4);
// samples to time
for (int i=0, offset=0; i<array->size(); i++) {
double x = (double) i - 0.5;
double y = dt * (double)(*array)[i];
xaxis[offset] = x +0.05;
yaxis[offset] = 0;
offset++;
xaxis[offset] = x+0.05;
yaxis[offset] = y;
offset++;
xaxis[offset] = x+0.95;
yaxis[offset] = y;
offset++;
xaxis[offset] = x +0.95;
yaxis[offset] = 0;
offset++;
}
for (int i=0, offset=0; i<selectedArray->size(); i++) {
double x = (double)i - 0.5;
double y = dt * (double)(*selectedArray)[i];
selectedxaxis[offset] = x +0.05;
selectedyaxis[offset] = 0;
offset++;
selectedxaxis[offset] = x+0.05;
selectedyaxis[offset] = y;
offset++;
selectedxaxis[offset] = x+0.95;
selectedyaxis[offset] = y;
offset++;
selectedxaxis[offset] = x +0.95;
selectedyaxis[offset] = 0;
offset++;
}
if (!absolutetime) {
percentify(yaxis, 2);
percentify(selectedyaxis, 2);
}
// set those curves
curve->setData(xaxis.data(), yaxis.data(), xaxis.size());
// Opaque - we don't need to show zone shading
QBrush brush = curve->brush();
QColor bcol = brush.color();
bcol.setAlpha(200);
brush.setColor(bcol);
curve->setBrush(brush);
curveSelected->setData(selectedxaxis.data(), selectedyaxis.data(), selectedxaxis.size());
// zone scale draw
if (series == RideFile::watts && zoned && rideItem && rideItem->zones) {
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 && mainWindow->hrZones() &&
(hrRange=mainWindow->hrZones()->whichRange(rideItem->dateTime.date())) != -1) {
setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(mainWindow->hrZones(), hrRange));
if (hrRange >= 0)
setAxisScale(QwtPlot::xBottom, -0.99, mainWindow->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 && mainWindow->zones()) {
setAxisScaleDraw(QwtPlot::xBottom, new ZoneScaleDraw(mainWindow->zones(), 0));
if (mainWindow->zones()->getRangeSize())
setAxisScale(QwtPlot::xBottom, -0.99, mainWindow->zones()->numZones(0), 1); // XXX use zones from first defined range
}
// hr zoned for a time range
if (source == Cache && zoned && series == RideFile::hr && mainWindow->hrZones()) {
setAxisScaleDraw(QwtPlot::xBottom, new HrZoneScaleDraw(mainWindow->hrZones(), 0));
if (mainWindow->hrZones()->getRangeSize())
setAxisScale(QwtPlot::xBottom, -0.99, mainWindow->hrZones()->numZones(0), 1); // XXX use zones from first defined range
}
setAxisMaxMinor(QwtPlot::xBottom, 0);
}
setYMax();
replot();
}
void
PowerHist::setYMax()
{
double 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);
}
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
wattsArray.resize(0);
wattsZoneArray.resize(10);
nmArray.resize(0);
hrArray.resize(0);
hrZoneArray.resize(10);
kphArray.resize(0);
cadArray.resize(0);
// we do not use the selected array since it is
// not meaningful to overlay interval selection
// with long term data
wattsSelectedArray.resize(0);
wattsZoneSelectedArray.resize(0);
nmSelectedArray.resize(0);
hrSelectedArray.resize(0);
hrZoneSelectedArray.resize(0);
kphSelectedArray.resize(0);
cadSelectedArray.resize(0);
longFromDouble(wattsArray, cache->distributionArray(RideFile::watts));
longFromDouble(hrArray, cache->distributionArray(RideFile::hr));
longFromDouble(nmArray, cache->distributionArray(RideFile::nm));
longFromDouble(cadArray, cache->distributionArray(RideFile::cad));
longFromDouble(kphArray, cache->distributionArray(RideFile::kph));
if (!useMetricUnits) {
double torque_factor = (useMetricUnits ? 1.0 : 0.73756215);
double speed_factor = (useMetricUnits ? 1.0 : 0.62137119);
for(int i=0; i<nmArray.size(); i++) nmArray[i] = nmArray[i] * torque_factor;
for(int i=0; i<kphArray.size(); i++) kphArray[i] = kphArray[i] * speed_factor;
}
// zone array
for (int i=0; i<10; i++) {
wattsZoneArray[i] = cache->wattsZoneArray()[i];
hrZoneArray[i] = cache->hrZoneArray()[i];
}
}
void
PowerHist::setData(RideItem *_rideItem, bool force)
{
source = Ride;
// 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 && ride->areDataPresent()->watts) ||
(series == RideFile::nm && ride->areDataPresent()->nm) ||
(series == RideFile::kph && ride->areDataPresent()->kph) ||
(series == RideFile::cad && ride->areDataPresent()->cad) ||
(series == RideFile::hr && ride->areDataPresent()->hr);
if (ride && hasData) {
//setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));
static const int maxSize = 4096;
// recording interval in minutes
dt = ride->recIntSecs() / 60.0;
wattsArray.resize(0);
wattsZoneArray.resize(0);
nmArray.resize(0);
hrArray.resize(0);
hrZoneArray.resize(0);
kphArray.resize(0);
cadArray.resize(0);
wattsSelectedArray.resize(0);
wattsZoneSelectedArray.resize(0);
nmSelectedArray.resize(0);
hrSelectedArray.resize(0);
hrZoneSelectedArray.resize(0);
kphSelectedArray.resize(0);
cadSelectedArray.resize(0);
// unit conversion factor for imperial units for selected parameters
double torque_factor = (useMetricUnits ? 1.0 : 0.73756215);
double speed_factor = (useMetricUnits ? 1.0 : 0.62137119);
foreach(const RideFilePoint *p1, ride->dataPoints()) {
bool selected = isSelected(p1, ride->recIntSecs());
// watts array
int wattsIndex = int(floor(p1->watts / wattsDelta));
if (wattsIndex >= 0 && wattsIndex < maxSize) {
if (wattsIndex >= wattsArray.size())
wattsArray.resize(wattsIndex + 1);
wattsArray[wattsIndex]++;
if (selected) {
if (wattsIndex >= wattsSelectedArray.size())
wattsSelectedArray.resize(wattsIndex + 1);
wattsSelectedArray[wattsIndex]++;
}
}
// watts zoned array
const Zones *zones = rideItem->zones;
int zoneRange = zones ? zones->whichRange(ride->startTime().date()) : -1;
// Only calculate zones if we have a valid range and check zeroes
if (zoneRange > -1 && (withz || (!withz && p1->watts))) {
wattsIndex = zones->whichZone(zoneRange, p1->watts);
if (wattsIndex >= 0 && wattsIndex < maxSize) {
if (wattsIndex >= wattsZoneArray.size())
wattsZoneArray.resize(wattsIndex + 1);
wattsZoneArray[wattsIndex]++;
if (selected) {
if (wattsIndex >= wattsZoneSelectedArray.size())
wattsZoneSelectedArray.resize(wattsIndex + 1);
wattsZoneSelectedArray[wattsIndex]++;
}
}
}
int nmIndex = int(floor(p1->nm * torque_factor / nmDelta));
if (nmIndex >= 0 && nmIndex < maxSize) {
if (nmIndex >= nmArray.size())
nmArray.resize(nmIndex + 1);
nmArray[nmIndex]++;
if (selected) {
if (nmIndex >= nmSelectedArray.size())
nmSelectedArray.resize(nmIndex + 1);
nmSelectedArray[nmIndex]++;
}
}
int hrIndex = int(floor(p1->hr / hrDelta));
if (hrIndex >= 0 && hrIndex < maxSize) {
if (hrIndex >= hrArray.size())
hrArray.resize(hrIndex + 1);
hrArray[hrIndex]++;
if (selected) {
if (hrIndex >= hrSelectedArray.size())
hrSelectedArray.resize(hrIndex + 1);
hrSelectedArray[hrIndex]++;
}
}
// hr zoned array
int hrZoneRange = mainWindow->hrZones() ? mainWindow->hrZones()->whichRange(ride->startTime().date()) : -1;
// Only calculate zones if we have a valid range
if (hrZoneRange > -1 && (withz || (!withz && p1->hr))) {
hrIndex = mainWindow->hrZones()->whichZone(hrZoneRange, p1->hr);
if (hrIndex >= 0 && hrIndex < maxSize) {
if (hrIndex >= hrZoneArray.size())
hrZoneArray.resize(hrIndex + 1);
hrZoneArray[hrIndex]++;
if (selected) {
if (hrIndex >= hrZoneSelectedArray.size())
hrZoneSelectedArray.resize(hrIndex + 1);
hrZoneSelectedArray[hrIndex]++;
}
}
}
int kphIndex = int(floor(p1->kph * speed_factor / kphDelta));
if (kphIndex >= 0 && kphIndex < maxSize) {
if (kphIndex >= kphArray.size())
kphArray.resize(kphIndex + 1);
kphArray[kphIndex]++;
if (selected) {
if (kphIndex >= kphSelectedArray.size())
kphSelectedArray.resize(kphIndex + 1);
kphSelectedArray[kphIndex]++;
}
}
int cadIndex = int(floor(p1->cad / cadDelta));
if (cadIndex >= 0 && cadIndex < maxSize) {
if (cadIndex >= cadArray.size())
cadArray.resize(cadIndex + 1);
cadArray[cadIndex]++;
if (selected) {
if (cadIndex >= cadSelectedArray.size())
cadSelectedArray.resize(cadIndex + 1);
cadSelectedArray[cadIndex]++;
}
}
}
} else {
// create empty curves when no data
const double zero = 0;
curve->setData(&zero, &zero, 0);
curveSelected->setData(&zero, &zero, 0);
replot();
}
zoomer->setZoomBase();
}
void
PowerHist::setBinWidth(int value)
{
if (!value) value = 1; // binwidth must be nonzero
binw = value;
appsettings->setValue(GC_HIST_BIN_WIDTH, value);
}
void
PowerHist::setZoned(bool value)
{
zoned = value;
}
double
PowerHist::getDelta()
{
switch (series) {
case RideFile::watts: return wattsDelta;
case RideFile::nm: return nmDelta;
case RideFile::hr: return hrDelta;
case RideFile::kph: return kphDelta;
case RideFile::cad: return cadDelta;
default: return 1;
}
}
int
PowerHist::getDigits()
{
switch (series) {
case RideFile::watts: return wattsDigits;
case RideFile::nm: return nmDigits;
case RideFile::hr: return hrDigits;
case RideFile::kph: return kphDigits;
case RideFile::cad: return cadDigits;
default: return 1;
}
}
int
PowerHist::setBinWidthRealUnits(double value)
{
setBinWidth(round(value / getDelta()));
if (!binw) binw = 1; // must be nonzero
return binw;
}
double
PowerHist::getBinWidthRealUnits()
{
return binw * getDelta();
}
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 QwtLog10ScaleEngine);
curve->setBaseline(1e-6);
} else {
setAxisScaleEngine(yLeft, new QwtLinearScaleEngine);
curve->setBaseline(0);
}
setYMax();
replot();
}
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::hr:
if (zoned) axislabel = tr("Heartrate zone");
else axislabel = tr("Heartrate (bpm)");
break;
case RideFile::cad:
axislabel = tr("Cadence (rpm)");
break;
case RideFile::kph:
axislabel = QString(tr("Speed (%1)")).arg(useMetricUnits ? tr("kph") : tr("mph"));
break;
case RideFile::nm:
axislabel = QString(tr("Torque (%1)")).arg(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::watts && !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 (mainWindow->allIntervalItems() != NULL) {
for (int i=0; i<mainWindow->allIntervalItems()->childCount(); i++) {
IntervalItem *current = dynamic_cast<IntervalItem*>(mainWindow->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->x(index);
double yvalue = curve->y(index);
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) {
// output the tooltip
text = QString("%1 %2\n%3 %4")
.arg(xvalue, 0, 'f', getDigits())
.arg(this->axisTitle(curve->xAxis()).text())
.arg(yvalue, 0, 'f', 1)
.arg(absolutetime ? tr("minutes") : tr("%"));
// 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;
}