mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
.. add W'bal distribution to Histogram - for a ride - for a date range - for a compared ride - for a compared date range .. still need to revisit to do in zones (3b) .. still need to update ridesummary to show summary of w'bal zone distribution (3c)
1139 lines
34 KiB
C++
1139 lines
34 KiB
C++
/*
|
|
* Copyright (c) 2009 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 "HistogramWindow.h"
|
|
#include "Specification.h"
|
|
#include "HelpWhatsThis.h"
|
|
|
|
// 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 double wbalDelta = 1.0;
|
|
static const double gearDelta = 0.01; //RideFileCache creates POW(10) * decimals sections
|
|
|
|
// digits for text entry validator
|
|
static const int wattsDigits = 0;
|
|
static const int wbalDigits = 0;
|
|
static const int wattsKgDigits = 2;
|
|
static const int nmDigits = 1;
|
|
static const int hrDigits = 0;
|
|
static const int kphDigits = 1;
|
|
static const int cadDigits = 0;
|
|
static const int gearDigits = 2;
|
|
|
|
|
|
//
|
|
// Constructor
|
|
//
|
|
HistogramWindow::HistogramWindow(Context *context, bool rangemode) : GcChartWindow(context), context(context), stale(true), source(NULL), active(false), bactive(false), rangemode(rangemode), compareStale(false), useCustom(false), useToToday(false), precision(99)
|
|
{
|
|
|
|
QWidget *c = new QWidget;
|
|
c->setContentsMargins(0,0,0,0);
|
|
HelpWhatsThis *helpConfig = new HelpWhatsThis(c);
|
|
if (rangemode) c->setWhatsThis(helpConfig->getWhatsThisText(HelpWhatsThis::ChartTrends_Distribution));
|
|
else c->setWhatsThis(helpConfig->getWhatsThisText(HelpWhatsThis::ChartRides_Histogram));
|
|
QFormLayout *cl = new QFormLayout(c);
|
|
cl->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
|
cl->setSpacing(5);
|
|
setControls(c);
|
|
|
|
//
|
|
// reveal controls widget
|
|
//
|
|
|
|
// reveal controls
|
|
rWidth = new QLabel(tr("Bin Width"));
|
|
rBinEdit = new QLineEdit();
|
|
rBinEdit->setFixedWidth(40);
|
|
rBinSlider = new QSlider(Qt::Horizontal);
|
|
rBinSlider->setTickPosition(QSlider::TicksBelow);
|
|
rBinSlider->setTickInterval(10);
|
|
rBinSlider->setMinimum(1);
|
|
rBinSlider->setMaximum(100);
|
|
rShade = new QCheckBox(tr("Shade zones"));
|
|
rZones = new QCheckBox(tr("Show in zones"));
|
|
|
|
// layout reveal controls
|
|
QHBoxLayout *r = new QHBoxLayout;
|
|
r->setContentsMargins(0,0,0,0);
|
|
r->addStretch();
|
|
r->addWidget(rWidth);
|
|
r->addWidget(rBinEdit);
|
|
r->addWidget(rBinSlider);
|
|
QVBoxLayout *v = new QVBoxLayout;
|
|
v->addWidget(rShade);
|
|
v->addWidget(rZones);
|
|
r->addSpacing(20);
|
|
r->addLayout(v);
|
|
r->addStretch();
|
|
setRevealLayout(r);
|
|
|
|
// plot
|
|
QVBoxLayout *vlayout = new QVBoxLayout;
|
|
vlayout->setSpacing(10);
|
|
powerHist = new PowerHist(context, rangemode);
|
|
vlayout->addWidget(powerHist);
|
|
|
|
HelpWhatsThis *help = new HelpWhatsThis(powerHist);
|
|
if (rangemode) powerHist->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::ChartTrends_Distribution));
|
|
else powerHist->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::ChartRides_Histogram));
|
|
|
|
setChartLayout(vlayout);
|
|
|
|
// search filter box
|
|
isfiltered = false;
|
|
searchBox = new SearchFilterBox(this, context);
|
|
connect(searchBox, SIGNAL(searchClear()), this, SLOT(clearFilter()));
|
|
connect(searchBox, SIGNAL(searchResults(QStringList)), this, SLOT(setFilter(QStringList)));
|
|
if (!rangemode) searchBox->hide();
|
|
else {
|
|
cl->addRow(new QLabel(tr("Filter")), searchBox);
|
|
cl->addWidget(new QLabel(""));
|
|
}
|
|
HelpWhatsThis *searchHelp = new HelpWhatsThis(searchBox);
|
|
searchBox->setWhatsThis(searchHelp->getWhatsThisText(HelpWhatsThis::SearchFilterBox));
|
|
|
|
// date selection
|
|
dateSetting = new DateSettingsEdit(this);
|
|
HelpWhatsThis *dateSettingHelp = new HelpWhatsThis(dateSetting);
|
|
dateSetting->setWhatsThis(dateSettingHelp->getWhatsThisText(HelpWhatsThis::ChartTrends_DateRange));
|
|
|
|
if (rangemode) {
|
|
|
|
cl->addRow(new QLabel(tr("Date Range")), dateSetting);
|
|
cl->addWidget(new QLabel("")); // spacing
|
|
|
|
// default to data series!
|
|
data = new QRadioButton(tr("Data Samples"));
|
|
metric = new QRadioButton(tr("Metrics"));
|
|
data->setChecked(true);
|
|
metric->setChecked(false);
|
|
QHBoxLayout *radios = new QHBoxLayout;
|
|
radios->addWidget(data);
|
|
radios->addWidget(metric);
|
|
cl->addRow(new QLabel(tr("Plot")), radios);
|
|
|
|
connect(data, SIGNAL(toggled(bool)), this, SLOT(dataToggled(bool)));
|
|
connect(metric, SIGNAL(toggled(bool)), this, SLOT(metricToggled(bool)));
|
|
}
|
|
|
|
// data series
|
|
seriesCombo = new QComboBox();
|
|
addSeries();
|
|
if (rangemode) comboLabel = new QLabel("");
|
|
else comboLabel = new QLabel(tr("Data Series"));
|
|
cl->addRow(comboLabel, seriesCombo);
|
|
|
|
if (rangemode) {
|
|
|
|
// TOTAL METRIC
|
|
totalMetricTree = new QTreeWidget;
|
|
#ifdef Q_OS_MAC
|
|
totalMetricTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
totalMetricTree->setColumnCount(2);
|
|
totalMetricTree->setColumnHidden(1, true);
|
|
totalMetricTree->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
totalMetricTree->header()->hide();
|
|
//totalMetricTree->setFrameStyle(QFrame::NoFrame);
|
|
//totalMetricTree->setAlternatingRowColors (true);
|
|
totalMetricTree->setIndentation(5);
|
|
totalMetricTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
// ALL METRIC
|
|
distMetricTree = new QTreeWidget;
|
|
#ifdef Q_OS_MAC
|
|
distMetricTree->setAttribute(Qt::WA_MacShowFocusRect, 0);
|
|
#endif
|
|
distMetricTree->setColumnCount(2);
|
|
distMetricTree->setColumnHidden(1, true);
|
|
distMetricTree->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
distMetricTree->header()->hide();
|
|
//distMetricTree->setFrameStyle(QFrame::NoFrame);
|
|
distMetricTree->setIndentation(5);
|
|
distMetricTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
// add them all
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
for (int i = 0; i < factory.metricCount(); ++i) {
|
|
|
|
const RideMetric *m = factory.rideMetric(factory.metricName(i));
|
|
|
|
QTextEdit processHTML(m->name()); // process html encoding of(TM)
|
|
QString processed = processHTML.toPlainText();
|
|
|
|
QTreeWidgetItem *add;
|
|
add = new QTreeWidgetItem(distMetricTree->invisibleRootItem());
|
|
add->setText(0, processed);
|
|
add->setText(1, m->symbol());
|
|
|
|
// we only want totalising metrics
|
|
if (m->type() != RideMetric::Total) continue;
|
|
|
|
add = new QTreeWidgetItem(totalMetricTree->invisibleRootItem());
|
|
add->setText(0, processed);
|
|
add->setText(1, m->symbol());
|
|
}
|
|
|
|
QHBoxLayout *labels = new QHBoxLayout;
|
|
|
|
metricLabel1 = new QLabel(tr("Total (x-axis)"));
|
|
labels->addWidget(metricLabel1);
|
|
metricLabel1->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
|
|
metricLabel2 = new QLabel(tr("Distribution (y-axis)"));
|
|
metricLabel2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
|
labels->addWidget(metricLabel2);
|
|
|
|
cl->addRow((blankLabel1=new QLabel("")), labels);
|
|
QHBoxLayout *trees = new QHBoxLayout;
|
|
trees->addWidget(totalMetricTree);
|
|
trees->addWidget(distMetricTree);
|
|
cl->addRow((blankLabel2 = new QLabel("")), trees);
|
|
|
|
colorButton = new ColorButton(this, "Color", QColor(Qt::blue));
|
|
colorLabel = new QLabel(tr("Color"));
|
|
connect(colorButton, SIGNAL(colorChosen(QColor)), this, SLOT(updateChart()));
|
|
cl->addRow(colorLabel, colorButton);
|
|
|
|
// by default select number of rides by duration
|
|
// which are the metrics workout_time and ride_count
|
|
selectTotal("ride_count");
|
|
selectMetric("workout_time");
|
|
}
|
|
|
|
showSumY = new QComboBox();
|
|
showSumY->addItem(tr("Absolute Time"));
|
|
showSumY->addItem(tr("Percentage Time"));
|
|
|
|
showLabel = new QLabel(tr("Show"));
|
|
cl->addRow(showLabel, showSumY);
|
|
|
|
showLnY = new QCheckBox;
|
|
showLnY->setText(tr("Log Y"));
|
|
cl->addRow(blankLabel3 = new QLabel(""), showLnY);
|
|
|
|
showZeroes = new QCheckBox;
|
|
showZeroes->setText(tr("With zeros"));
|
|
cl->addRow(blankLabel4 = new QLabel(""), showZeroes);
|
|
|
|
shadeZones = new QCheckBox;
|
|
shadeZones->setText(tr("Shade zones"));
|
|
cl->addRow(blankLabel5 = new QLabel(""), shadeZones);
|
|
|
|
showInZones = new QCheckBox;
|
|
showInZones->setText(tr("Show in zones"));
|
|
cl->addRow(blankLabel6 = new QLabel(""), showInZones);
|
|
|
|
showInCPZones = new QCheckBox;
|
|
showInCPZones->setText(tr("Use polarised zones"));
|
|
cl->addRow(blankLabel7 = new QLabel(""), showInCPZones);
|
|
|
|
// bin width
|
|
QHBoxLayout *binWidthLayout = new QHBoxLayout;
|
|
QLabel *binWidthLabel = new QLabel(tr("Bin width"), this);
|
|
binWidthLineEdit = new QLineEdit(this);
|
|
binWidthLineEdit->setFixedWidth(40);
|
|
|
|
binWidthLayout->addWidget(binWidthLineEdit);
|
|
binWidthSlider = new QSlider(Qt::Horizontal);
|
|
binWidthSlider->setTickPosition(QSlider::TicksBelow);
|
|
binWidthSlider->setTickInterval(1);
|
|
binWidthSlider->setMinimum(1);
|
|
binWidthSlider->setMaximum(100);
|
|
binWidthLayout->addWidget(binWidthSlider);
|
|
cl->addRow(binWidthLabel, binWidthLayout);
|
|
|
|
// sort out default values
|
|
setBinEditors();
|
|
showLnY->setChecked(powerHist->islnY());
|
|
showZeroes->setChecked(powerHist->withZeros());
|
|
shadeZones->setChecked(powerHist->shade);
|
|
binWidthSlider->setValue(powerHist->binWidth());
|
|
rBinSlider->setValue(powerHist->binWidth());
|
|
rShade->setChecked(powerHist->shade);
|
|
|
|
// fixup series selected by default
|
|
seriesChanged();
|
|
|
|
// hide/show according to default mode
|
|
switchMode(); // does nothing if not in rangemode
|
|
|
|
// set the defaults etc
|
|
updateChart();
|
|
|
|
// the bin slider/input update each other
|
|
// only the input box triggers an update to the chart
|
|
connect(binWidthSlider, SIGNAL(valueChanged(int)), this, SLOT(setBinWidthFromSlider()));
|
|
connect(binWidthLineEdit, SIGNAL(editingFinished()), this, SLOT(setBinWidthFromLineEdit()));
|
|
connect(rBinSlider, SIGNAL(valueChanged(int)), this, SLOT(setrBinWidthFromSlider()));
|
|
connect(rBinEdit, SIGNAL(editingFinished()), this, SLOT(setrBinWidthFromLineEdit()));
|
|
connect(rZones, SIGNAL(stateChanged(int)), this, SLOT(setZoned(int)));
|
|
connect(rShade, SIGNAL(stateChanged(int)), this, SLOT(setShade(int)));
|
|
|
|
// when season changes we need to retrieve data from the cache then update the chart
|
|
if (rangemode) {
|
|
connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange)));
|
|
connect(dateSetting, SIGNAL(useCustomRange(DateRange)), this, SLOT(useCustomRange(DateRange)));
|
|
connect(dateSetting, SIGNAL(useThruToday()), this, SLOT(useThruToday()));
|
|
connect(dateSetting, SIGNAL(useStandardRange()), this, SLOT(useStandardRange()));
|
|
connect(distMetricTree, SIGNAL(itemSelectionChanged()), this, SLOT(treeSelectionChanged()));
|
|
connect(totalMetricTree, SIGNAL(itemSelectionChanged()), this, SLOT(treeSelectionChanged()));
|
|
|
|
// replot when background refresh is progressing
|
|
connect(context, SIGNAL(refreshUpdate(QDate)), this, SLOT(refreshUpdate(QDate)));
|
|
|
|
// comparing things
|
|
connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged()));
|
|
connect(context, SIGNAL(compareDateRangesChanged()), this, SLOT(compareChanged()));
|
|
|
|
} else {
|
|
dateSetting->hide();
|
|
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
|
|
connect(context, SIGNAL(rideChanged(RideItem*)), this, SLOT(forceReplot()));
|
|
connect(context, SIGNAL(intervalSelected()), this, SLOT(intervalSelected()));
|
|
connect(context, SIGNAL(intervalHover(IntervalItem*)), powerHist, SLOT(intervalHover(IntervalItem*)));
|
|
|
|
// comparing things
|
|
connect(context, SIGNAL(compareIntervalsStateChanged(bool)), this, SLOT(compareChanged()));
|
|
connect(context, SIGNAL(compareIntervalsChanged()), this, SLOT(compareChanged()));
|
|
}
|
|
|
|
// if any of the controls change we pass the chart everything
|
|
connect(showLnY, SIGNAL(stateChanged(int)), this, SLOT(forceReplot()));
|
|
connect(showZeroes, SIGNAL(stateChanged(int)), this, SLOT(forceReplot()));
|
|
connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(seriesChanged()));
|
|
connect(showInCPZones, SIGNAL(stateChanged(int)), this, SLOT(setCPZoned(int)));
|
|
connect(showInCPZones, SIGNAL(stateChanged(int)), this, SLOT(forceReplot()));
|
|
connect(showInZones, SIGNAL(stateChanged(int)), this, SLOT(setZoned(int)));
|
|
connect(showInZones, SIGNAL(stateChanged(int)), this, SLOT(forceReplot()));
|
|
connect(shadeZones, SIGNAL(stateChanged(int)), this, SLOT(setShade(int)));
|
|
connect(shadeZones, SIGNAL(stateChanged(int)), this, SLOT(forceReplot()));
|
|
connect(showSumY, SIGNAL(currentIndexChanged(int)), this, SLOT(forceReplot()));
|
|
|
|
connect(context->athlete, SIGNAL(zonesChanged()), this, SLOT(zonesChanged()));
|
|
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
|
|
connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(rideAddorRemove(RideItem*)));
|
|
connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(rideAddorRemove(RideItem*)));
|
|
connect(context, SIGNAL(rideSaved(RideItem*)), this, SLOT(rideAddorRemove(RideItem*)));
|
|
connect(context, SIGNAL(filterChanged()), this, SLOT(forceReplot()));
|
|
connect(context, SIGNAL(homeFilterChanged()), this, SLOT(forceReplot()));
|
|
|
|
// set colors etc
|
|
configChanged(CONFIG_APPEARANCE);
|
|
}
|
|
|
|
void
|
|
HistogramWindow::configChanged(qint32 state)
|
|
{
|
|
if (!rangemode) setProperty("color", GColor(CPLOTBACKGROUND)); // called on config change
|
|
else setProperty("color", GColor(CTRENDPLOTBACKGROUND)); // called on config change
|
|
powerHist->configChanged(state);
|
|
}
|
|
|
|
bool
|
|
HistogramWindow::isCompare() const
|
|
{
|
|
// compare intervals ?
|
|
if (!rangemode && context->isCompareIntervals) return true;
|
|
|
|
// compare date ranges ?
|
|
if (rangemode && context->isCompareDateRanges) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
HistogramWindow::compareChanged()
|
|
{
|
|
stale = true; // the 'standard' plots will need to be updated
|
|
compareStale = true;
|
|
|
|
if (!isVisible()) return;
|
|
|
|
setUpdatesEnabled(false);
|
|
|
|
// to stop getting into an infinite loop
|
|
// when turning off coimpare mode and using
|
|
// rideSelected to refresh
|
|
compareStale = false;
|
|
|
|
if (isCompare()) {
|
|
|
|
// is blank?
|
|
setIsBlank(false); // current ride irrelevant!
|
|
|
|
// hide normal curves
|
|
powerHist->hideStandard(true);
|
|
|
|
// now set the controls
|
|
RideFile::SeriesType series = static_cast<RideFile::SeriesType>
|
|
(seriesCombo->itemData(seriesCombo->currentIndex()).toInt());
|
|
powerHist->setSeries(series);
|
|
|
|
// and now the controls
|
|
powerHist->setShading(shadeZones->isChecked() ? true : false);
|
|
powerHist->setZoned(showInZones->isChecked() ? true : false);
|
|
powerHist->setCPZoned(showInCPZones->isChecked() ? true : false);
|
|
powerHist->setlnY(showLnY->isChecked() ? true : false);
|
|
powerHist->setWithZeros(showZeroes->isChecked() ? true : false);
|
|
powerHist->setSumY(showSumY->currentIndex()== 0 ? true : false);
|
|
|
|
// set data and create empty curves
|
|
if (!rangemode || data->isChecked()) {
|
|
|
|
// using the bests (ride file cache)
|
|
powerHist->setDataFromCompare();
|
|
|
|
} else {
|
|
// using the metric arrays
|
|
powerHist->setDelta(getDelta());
|
|
powerHist->setDigits(getDigits());
|
|
powerHist->setDataFromCompare(totalMetric(), distMetric());
|
|
}
|
|
powerHist->recalcCompare();
|
|
|
|
} else {
|
|
|
|
// show our normal curves and wipe rest
|
|
powerHist->hideStandard(false);
|
|
rideSelected(); // back to where we were
|
|
}
|
|
|
|
// replot!
|
|
powerHist->replot();
|
|
|
|
// repaint (in case optimised out)
|
|
repaint();
|
|
|
|
// and we're done
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
//
|
|
// Colors (used by metric plotting)
|
|
//
|
|
QString HistogramWindow::plotColor() const
|
|
{
|
|
if (!rangemode) return QColor(Qt::blue).name();
|
|
else {
|
|
return colorButton->getColor().name();
|
|
}
|
|
}
|
|
|
|
void HistogramWindow::setPlotColor(QString color)
|
|
{
|
|
if (rangemode) colorButton->setColor(QColor(color));
|
|
}
|
|
|
|
//
|
|
// Set Bin Width property by setting the slider
|
|
//
|
|
void HistogramWindow::setBin(double x)
|
|
{
|
|
if (bactive) return;
|
|
bactive = true;
|
|
binWidthSlider->setValue(x/getDelta());
|
|
rBinSlider->setValue(x/getDelta());
|
|
binWidthLineEdit->setText(QString("%1").arg(x, 0, 'f', getDigits()));
|
|
rBinEdit->setText(QString("%1").arg(x, 0, 'f', getDigits()));
|
|
powerHist->setBinWidth(x);
|
|
bactive = false;
|
|
|
|
// redraw
|
|
stale = true;
|
|
updateChart();
|
|
}
|
|
|
|
//
|
|
// Get/Set the metric selections
|
|
//
|
|
QString HistogramWindow::distMetric() const
|
|
{
|
|
if (!rangemode) return "workout_time";
|
|
else {
|
|
// get current selection
|
|
QList<QTreeWidgetItem *> select = distMetricTree->selectedItems();
|
|
if (select.count() == 0) return "workout_time";
|
|
else return select[0]->text(1);
|
|
}
|
|
}
|
|
|
|
void HistogramWindow::setDistMetric(QString value)
|
|
{
|
|
if (!rangemode) return;
|
|
selectMetric(value);
|
|
}
|
|
|
|
QString HistogramWindow::totalMetric() const
|
|
{
|
|
if (!rangemode) return "ride_count";
|
|
else {
|
|
// get current selection
|
|
QList<QTreeWidgetItem *> select = totalMetricTree->selectedItems();
|
|
if (select.count() == 0) return "ride_count";
|
|
else return select[0]->text(1);
|
|
}
|
|
}
|
|
|
|
void HistogramWindow::setTotalMetric(QString value)
|
|
{
|
|
if (!rangemode) return;
|
|
selectTotal(value);
|
|
}
|
|
|
|
void
|
|
HistogramWindow::selectTotal(QString symbol)
|
|
{
|
|
QList<QTreeWidgetItem *> found = totalMetricTree->findItems(symbol, Qt::MatchRecursive|Qt::MatchExactly, 1);
|
|
if (found.count() == 0) return;
|
|
|
|
// clear the current selection
|
|
foreach(QTreeWidgetItem *selected, totalMetricTree->selectedItems()) selected->setSelected(false);
|
|
|
|
// select the first one (there shouldn't be more than that!!!
|
|
found[0]->setSelected(true);
|
|
|
|
// make sure it is the current item and visible
|
|
totalMetricTree->setCurrentItem(found[0]);
|
|
totalMetricTree->scrollToItem(totalMetricTree->currentItem());
|
|
}
|
|
|
|
void
|
|
HistogramWindow::selectMetric(QString symbol)
|
|
{
|
|
QList<QTreeWidgetItem *> found = distMetricTree->findItems(symbol, Qt::MatchRecursive|Qt::MatchExactly, 1);
|
|
if (found.count() == 0) return;
|
|
|
|
// clear the current selection
|
|
foreach(QTreeWidgetItem *selected, distMetricTree->selectedItems()) selected->setSelected(false);
|
|
|
|
// select the first one (there shouldn't be more than that!!!
|
|
found[0]->setSelected(true);
|
|
|
|
// make sure it is the current item and visible
|
|
distMetricTree->setCurrentItem(found[0]);
|
|
distMetricTree->scrollToItem(distMetricTree->currentItem());
|
|
}
|
|
|
|
//
|
|
// get set mode (data series or metric?) -- only in rangemode
|
|
//
|
|
bool HistogramWindow::dataMode() const
|
|
{
|
|
if (!rangemode) return true;
|
|
else return data->isChecked();
|
|
}
|
|
|
|
void HistogramWindow::setDataMode(bool value)
|
|
{
|
|
if (!rangemode) return;
|
|
else {
|
|
active = true;
|
|
data->setChecked(value);
|
|
metric->setChecked(!value);
|
|
active = false;
|
|
switchMode();
|
|
}
|
|
}
|
|
|
|
//
|
|
// When user changes from data series to metric
|
|
//
|
|
void
|
|
HistogramWindow::dataToggled(bool x)
|
|
{
|
|
if (active) return;
|
|
|
|
active = true;
|
|
stale = true;
|
|
metric->setChecked(!x);
|
|
switchMode();
|
|
active = false;
|
|
}
|
|
|
|
void
|
|
HistogramWindow::metricToggled(bool x)
|
|
{
|
|
if (active) return;
|
|
|
|
active = true;
|
|
stale = true;
|
|
data->setChecked(!x);
|
|
switchMode();
|
|
active = false;
|
|
}
|
|
|
|
void
|
|
HistogramWindow::switchMode()
|
|
{
|
|
if (!rangemode) return; // ! only valid in rangemode
|
|
|
|
if (data->isChecked()) {
|
|
|
|
// hide all the metric controls
|
|
blankLabel1->hide();
|
|
blankLabel2->hide();
|
|
distMetricTree->hide();
|
|
totalMetricTree->hide();
|
|
metricLabel1->hide();
|
|
metricLabel2->hide();
|
|
colorLabel->hide();
|
|
colorButton->hide();
|
|
|
|
// show all the data series controls
|
|
comboLabel->show();
|
|
seriesCombo->show();
|
|
blankLabel3->show();
|
|
blankLabel4->show();
|
|
blankLabel5->show();
|
|
blankLabel6->show();
|
|
blankLabel7->show();
|
|
showSumY->show();
|
|
showLabel->show();
|
|
showLnY->show();
|
|
showZeroes->show();
|
|
shadeZones->show();
|
|
showInZones->show();
|
|
showInCPZones->show();
|
|
|
|
// select the series..
|
|
seriesChanged();
|
|
|
|
} else {
|
|
|
|
// hide all the data series controls
|
|
comboLabel->hide();
|
|
seriesCombo->hide();
|
|
blankLabel3->hide();
|
|
blankLabel4->hide();
|
|
blankLabel5->hide();
|
|
blankLabel6->hide();
|
|
blankLabel7->hide();
|
|
showSumY->hide();
|
|
showLabel->hide();
|
|
showLnY->hide();
|
|
showZeroes->hide();
|
|
shadeZones->hide();
|
|
showInZones->hide();
|
|
showInCPZones->hide();
|
|
|
|
// show all the metric controls
|
|
blankLabel1->show();
|
|
blankLabel2->show();
|
|
metricLabel1->show();
|
|
metricLabel2->show();
|
|
distMetricTree->show();
|
|
totalMetricTree->show();
|
|
colorLabel->show();
|
|
colorButton->show();
|
|
|
|
// select the series.. (but without the half second delay)
|
|
treeSelectionChanged();
|
|
}
|
|
|
|
stale = true;
|
|
updateChart(); // to whatever is currently selected.
|
|
}
|
|
|
|
|
|
//
|
|
// When user selects a new metric
|
|
//
|
|
void
|
|
HistogramWindow::treeSelectionChanged()
|
|
{
|
|
// new metric chosen, so set up all the bin width, line edit
|
|
// delta, precision etc
|
|
powerHist->setSeries(RideFile::none);
|
|
powerHist->setDelta(getDelta());
|
|
powerHist->setDigits(getDigits());
|
|
|
|
// now update the slider stepper and linedit
|
|
setBinEditors();
|
|
|
|
// initial value -- but only if need to
|
|
double minbinw = getDelta();
|
|
double maxbinw = getDelta() * 100;
|
|
double current = binWidthLineEdit->text().toDouble();
|
|
if (current < minbinw || current > maxbinw) setBin(getDelta());
|
|
|
|
// replot
|
|
updateChart();
|
|
}
|
|
|
|
//
|
|
// When user selects a different data series
|
|
//
|
|
void
|
|
HistogramWindow::seriesChanged()
|
|
{
|
|
// series changed so tell power hist
|
|
powerHist->setSeries(static_cast<RideFile::SeriesType>(seriesCombo->itemData(seriesCombo->currentIndex()).toInt()));
|
|
powerHist->setDelta(getDelta());
|
|
powerHist->setDigits(getDigits());
|
|
|
|
// now update the slider stepper and linedit
|
|
setBinEditors();
|
|
|
|
// set an initial bin width value
|
|
setBin(getDelta());
|
|
|
|
// replot
|
|
stale = true;
|
|
updateChart();
|
|
}
|
|
|
|
//
|
|
// We need to config / update the controls when data series/metrics change
|
|
//
|
|
void
|
|
HistogramWindow::setBinEditors()
|
|
{
|
|
// the line edit validator
|
|
QValidator *validator;
|
|
if (getDigits() == 0) {
|
|
|
|
validator = new QIntValidator(binWidthSlider->minimum() * getDelta(),
|
|
binWidthSlider->maximum() * getDelta(),
|
|
binWidthLineEdit);
|
|
} else {
|
|
|
|
validator = new QDoubleValidator(binWidthSlider->minimum() * getDelta(),
|
|
binWidthSlider->maximum() * getDelta(),
|
|
getDigits(),
|
|
binWidthLineEdit);
|
|
}
|
|
binWidthLineEdit->setValidator(validator);
|
|
rBinEdit->setValidator(validator);
|
|
}
|
|
|
|
//
|
|
// A new ride was selected
|
|
//
|
|
void
|
|
HistogramWindow::rideSelected()
|
|
{
|
|
if (!amVisible()) return;
|
|
|
|
RideItem *ride = myRideItem;
|
|
// handle catch up to compare changed
|
|
if (compareStale) compareChanged();
|
|
|
|
if (!ride || isCompare() || (rangemode && !stale)) return;
|
|
|
|
if (rangemode) {
|
|
// get range that applies to this ride
|
|
powerRange = context->athlete->zones()->whichRange(ride->dateTime.date());
|
|
hrRange = context->athlete->hrZones()->whichRange(ride->dateTime.date());
|
|
}
|
|
|
|
// update
|
|
updateChart();
|
|
}
|
|
|
|
//
|
|
// User selected a new interval
|
|
//
|
|
void
|
|
HistogramWindow::intervalSelected()
|
|
{
|
|
if (!amVisible()) return;
|
|
|
|
RideItem *ride = myRideItem;
|
|
|
|
// null? or not plotting current ride, ignore signal
|
|
if (!ride || isCompare() || rangemode) return;
|
|
|
|
// update
|
|
interval = true;
|
|
updateChart();
|
|
}
|
|
|
|
void
|
|
HistogramWindow::rideAddorRemove(RideItem *)
|
|
{
|
|
stale = true;
|
|
if (amVisible()) updateChart();
|
|
}
|
|
|
|
void
|
|
HistogramWindow::zonesChanged()
|
|
{
|
|
if (!amVisible()) return;
|
|
|
|
powerHist->refreshZoneLabels();
|
|
powerHist->replot();
|
|
}
|
|
|
|
void
|
|
HistogramWindow::useCustomRange(DateRange range)
|
|
{
|
|
// plot using the supplied range
|
|
useCustom = true;
|
|
useToToday = false;
|
|
custom = range;
|
|
dateRangeChanged(custom);
|
|
}
|
|
|
|
void
|
|
HistogramWindow::useStandardRange()
|
|
{
|
|
useToToday= useCustom = false;
|
|
dateRangeChanged(myDateRange);
|
|
}
|
|
|
|
void
|
|
HistogramWindow::useThruToday()
|
|
{
|
|
// plot using the supplied range
|
|
useCustom = false;
|
|
useToToday = true;
|
|
custom = myDateRange;
|
|
if (custom.to > QDate::currentDate()) custom.to = QDate::currentDate();
|
|
dateRangeChanged(custom);
|
|
}
|
|
|
|
void HistogramWindow::dateRangeChanged(DateRange dateRange)
|
|
{
|
|
// compare mode?
|
|
if (amVisible() && compareStale) compareChanged();
|
|
|
|
// if we're using a custom one lets keep it
|
|
if (rangemode && (useCustom || useToToday)) dateRange = custom;
|
|
|
|
// has it changed?
|
|
if (dateRange.from != cfrom || dateRange.to != cto)
|
|
stale = true;
|
|
|
|
// don't do it if we're invisible, in compare or
|
|
// nothing has changed since last time ..
|
|
if (!amVisible() || isCompare() || !stale) return;
|
|
|
|
updateChart();
|
|
}
|
|
|
|
void HistogramWindow::addSeries()
|
|
{
|
|
// setup series list
|
|
seriesList << RideFile::watts
|
|
<< RideFile::wattsKg
|
|
<< RideFile::hr
|
|
<< RideFile::kph
|
|
<< RideFile::cad
|
|
<< RideFile::nm
|
|
<< RideFile::aPower
|
|
<< RideFile::gear
|
|
<< RideFile::smo2
|
|
<< RideFile::wbal;
|
|
|
|
foreach (RideFile::SeriesType x, seriesList)
|
|
seriesCombo->addItem(RideFile::seriesName(x), static_cast<int>(x));
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setBinWidthFromSlider()
|
|
{
|
|
if (bactive) return;
|
|
setBin(binWidthSlider->value() * getDelta());
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setrBinWidthFromSlider()
|
|
{
|
|
if (bactive) return;
|
|
setBin(rBinSlider->value() * getDelta());
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setBinWidthFromLineEdit()
|
|
{
|
|
if (bactive) return;
|
|
setBin(binWidthLineEdit->text().toDouble());
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setrBinWidthFromLineEdit()
|
|
{
|
|
if (bactive) return;
|
|
setBin(rBinEdit->text().toDouble());
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setCPZoned(int x)
|
|
{
|
|
showInCPZones->setCheckState((Qt::CheckState)x);
|
|
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setZoned(int x)
|
|
{
|
|
rZones->setCheckState((Qt::CheckState)x);
|
|
showInZones->setCheckState((Qt::CheckState)x);
|
|
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setShade(int x)
|
|
{
|
|
rShade->setCheckState((Qt::CheckState)x);
|
|
shadeZones->setCheckState((Qt::CheckState)x);
|
|
}
|
|
|
|
void
|
|
HistogramWindow::refreshUpdate(QDate past)
|
|
{
|
|
if (!rangemode || cfrom > past || (lastupdate != QTime() && lastupdate.secsTo(QTime::currentTime()) < 5)) return;
|
|
lastupdate = QTime::currentTime();
|
|
forceReplot();
|
|
|
|
}
|
|
|
|
void
|
|
HistogramWindow::forceReplot()
|
|
{
|
|
stale = true;
|
|
if (amVisible()) updateChart();
|
|
}
|
|
|
|
void
|
|
HistogramWindow::updateChart()
|
|
{
|
|
// What is the selected series?
|
|
RideFile::SeriesType series = static_cast<RideFile::SeriesType>(seriesCombo->itemData(seriesCombo->currentIndex()).toInt());
|
|
|
|
// compare mode does its own thing so ignore
|
|
// this request (but set stale) if we're comparing
|
|
// we WILL get called when compare ends
|
|
if (!amVisible()) {
|
|
stale = true;
|
|
return;
|
|
}
|
|
|
|
// just reflect chart setting changes
|
|
if (isCompare()) {
|
|
|
|
compareChanged();
|
|
return;
|
|
}
|
|
|
|
// If no data present show the blank state page
|
|
if (!rangemode) {
|
|
RideFile::SeriesType baseSeries = (series == RideFile::wattsKg || series == RideFile::wbal) ? RideFile::watts : series;
|
|
if (rideItem() != NULL && rideItem()->ride()->isDataPresent(baseSeries))
|
|
setIsBlank(false);
|
|
else
|
|
setIsBlank(true);
|
|
} else {
|
|
setIsBlank(false);
|
|
}
|
|
|
|
// Lets get the data then
|
|
if (stale) {
|
|
|
|
RideFileCache *old = source;
|
|
|
|
if (rangemode) {
|
|
|
|
// set the date range to the appropriate selection
|
|
DateRange use;
|
|
if (useCustom) {
|
|
|
|
use = custom;
|
|
|
|
} else if (useToToday) {
|
|
|
|
use = myDateRange;
|
|
QDate today = QDate::currentDate();
|
|
if (use.to > today) use.to = today;
|
|
|
|
} else {
|
|
|
|
use = myDateRange;
|
|
}
|
|
|
|
if (data->isChecked()) {
|
|
|
|
// plotting a data series, so refresh the ridefilecache
|
|
|
|
source = new RideFileCache(context, use.from, use.to, isfiltered, files, rangemode);
|
|
cfrom = use.from;
|
|
cto = use.to;
|
|
stale = false;
|
|
|
|
if (old) delete old; // guarantee source pointer changes
|
|
stale = false; // well we tried
|
|
|
|
// and which series to plot
|
|
powerHist->setSeries(series);
|
|
|
|
// and now the controls
|
|
powerHist->setShading(shadeZones->isChecked() ? true : false);
|
|
powerHist->setZoned(showInZones->isChecked() ? true : false);
|
|
powerHist->setCPZoned(showInCPZones->isChecked() ? true : false);
|
|
powerHist->setlnY(showLnY->isChecked() ? true : false);
|
|
powerHist->setWithZeros(showZeroes->isChecked() ? true : false);
|
|
powerHist->setSumY(showSumY->currentIndex()== 0 ? true : false);
|
|
|
|
// set the data on the plot
|
|
powerHist->setData(source);
|
|
|
|
} else {
|
|
|
|
if (last.from != use.from || last.to != use.to) {
|
|
|
|
// remember the last lot we collected
|
|
last = use;
|
|
|
|
}
|
|
|
|
FilterSet fs;
|
|
fs.addFilter(isfiltered, files);
|
|
fs.addFilter(context->isfiltered, context->filters);
|
|
fs.addFilter(context->ishomefiltered, context->homeFilters);
|
|
|
|
// setData using the summary metrics -- always reset since filters may
|
|
// have changed, or perhaps the bin width...
|
|
powerHist->setSeries(RideFile::none);
|
|
powerHist->setDelta(getDelta());
|
|
powerHist->setDigits(getDigits());
|
|
powerHist->setData(Specification(use,fs), totalMetric(), distMetric(), &powerHist->standard);
|
|
powerHist->setColor(colorButton->getColor());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// and which series to plot
|
|
powerHist->setSeries(series);
|
|
|
|
// and now the controls
|
|
powerHist->setShading(shadeZones->isChecked() ? true : false);
|
|
powerHist->setZoned(showInZones->isChecked() ? true : false);
|
|
powerHist->setCPZoned(showInCPZones->isChecked() ? true : false);
|
|
powerHist->setlnY(showLnY->isChecked() ? true : false);
|
|
powerHist->setWithZeros(showZeroes->isChecked() ? true : false);
|
|
powerHist->setSumY(showSumY->currentIndex()== 0 ? true : false);
|
|
|
|
// do once the controls are set
|
|
powerHist->setData(myRideItem, true); // intervals selected forces data to
|
|
// be recomputed since interval selection
|
|
// has changed.
|
|
|
|
}
|
|
|
|
powerHist->recalc(true); // interval changed? force recalc
|
|
powerHist->replot();
|
|
|
|
interval = false;// we force a recalc when called coz intervals
|
|
// have been selected. The recalc routine in
|
|
// powerhist optimises out, but doesn't keep track
|
|
// of interval selection -- simplifies the setters
|
|
// and getters, so worth this 'hack'.
|
|
} // if stale
|
|
}
|
|
|
|
void
|
|
HistogramWindow::clearFilter()
|
|
{
|
|
isfiltered = false;
|
|
files.clear();
|
|
stale = true;
|
|
updateChart();
|
|
repaint();
|
|
}
|
|
|
|
void
|
|
HistogramWindow::setFilter(QStringList list)
|
|
{
|
|
isfiltered = true;
|
|
files = list;
|
|
stale = true;
|
|
updateChart();
|
|
repaint();
|
|
}
|
|
|
|
double
|
|
HistogramWindow::getDelta()
|
|
{
|
|
if (rangemode && !data->isChecked()) {
|
|
|
|
// based upon the metric chosen
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
const RideMetric *m = factory.rideMetric(distMetric());
|
|
|
|
if (m) return 1.00F / pow(10, m->precision());
|
|
else return 1;
|
|
|
|
|
|
} else {
|
|
|
|
// use the predefined delta
|
|
switch (seriesCombo->currentIndex()) {
|
|
case 0: return wattsDelta;
|
|
case 1: return wattsKgDelta;
|
|
case 2: return hrDelta;
|
|
case 3: return kphDelta;
|
|
case 4: return cadDelta;
|
|
case 5: return nmDelta;
|
|
case 6: return wattsDelta; //aPower
|
|
case 7: return gearDelta;
|
|
case 8: return wbalDelta;
|
|
default: return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
HistogramWindow::getDigits()
|
|
{
|
|
if (rangemode && !data->isChecked()) {
|
|
|
|
// based upon the metric chosen
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
const RideMetric *m = factory.rideMetric(distMetric());
|
|
|
|
if (m) return m->precision();
|
|
else return 0;
|
|
|
|
} else {
|
|
|
|
// use predefined for data series
|
|
switch (seriesCombo->currentIndex()) {
|
|
case 0: return wattsDigits;
|
|
case 1: return wattsKgDigits;
|
|
case 2: return hrDigits;
|
|
case 3: return kphDigits;
|
|
case 4: return cadDigits;
|
|
case 5: return nmDigits;
|
|
case 6: return wattsDigits; // aPower
|
|
case 7: return gearDigits;
|
|
case 8: return wbalDigits;
|
|
default: return 1;
|
|
}
|
|
}
|
|
}
|