mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
Don't refresh CP, LTM or Histogram plots when dateRange property is set, if the dateRange was previously plotted anyway. Will set to stale if a ride is added or deleted too. This means the replot of tabs is not performed when simply switching between tabs. But will if the date range does change or a ride is added or deleted meaning the aggregate needs to be re-calculated. There is still one more performance improvement required; 1. get RideFileCache to have a cache of recent aggregations. Since (a) the same ranges will be called over and over by different charts when a season is selected and (b) there are only likely to be 20-30 seasons defined in total, so lets cache them instead of recalculating every time.
404 lines
13 KiB
C++
404 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2009 Sean C. Rhea (srhea@srhea.net)
|
|
*
|
|
* 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 "CriticalPowerWindow.h"
|
|
#include "SearchFilterBox.h"
|
|
#include "CpintPlot.h"
|
|
#include "MainWindow.h"
|
|
#include "RideItem.h"
|
|
#include "TimeUtils.h"
|
|
#include <qwt_picker.h>
|
|
#include <qwt_picker_machine.h>
|
|
#include <qwt_plot_picker.h>
|
|
#include <qwt_compat.h>
|
|
#include <QFile>
|
|
#include "Season.h"
|
|
#include "SeasonParser.h"
|
|
#include "Colors.h"
|
|
#include <QXmlInputSource>
|
|
#include <QXmlSimpleReader>
|
|
|
|
CriticalPowerWindow::CriticalPowerWindow(const QDir &home, MainWindow *parent, bool rangemode) :
|
|
GcWindow(parent), _dateRange("{00000000-0000-0000-0000-000000000001}"), home(home), mainWindow(parent), currentRide(NULL), rangemode(rangemode), stale(true)
|
|
{
|
|
setInstanceName("Critical Power Window");
|
|
|
|
// main plot area
|
|
QVBoxLayout *vlayout = new QVBoxLayout;
|
|
cpintPlot = new CpintPlot(mainWindow, home.path(), mainWindow->zones());
|
|
vlayout->addWidget(cpintPlot);
|
|
setLayout(vlayout);
|
|
|
|
// controls
|
|
QWidget *c = new QWidget;
|
|
QVBoxLayout *cl = new QVBoxLayout(c);
|
|
setControls(c);
|
|
|
|
#ifdef GC_HAVE_LUCENE
|
|
// searchbox
|
|
searchBox = new SearchFilterBox(this, parent);
|
|
connect(searchBox, SIGNAL(searchClear()), cpintPlot, SLOT(clearFilter()));
|
|
connect(searchBox, SIGNAL(searchResults(QStringList)), cpintPlot, SLOT(setFilter(QStringList)));
|
|
connect(searchBox, SIGNAL(searchClear()), this, SLOT(filterChanged()));
|
|
connect(searchBox, SIGNAL(searchResults(QStringList)), this, SLOT(filterChanged()));
|
|
cl->addWidget(searchBox);
|
|
#endif
|
|
|
|
// picker details
|
|
QFormLayout *cpintPickerLayout = new QFormLayout;
|
|
QLabel *cpintTimeLabel = new QLabel(tr("Duration:"), this);
|
|
cpintTimeValue = new QLineEdit("0 s");
|
|
QLabel *cpintTodayLabel = new QLabel(tr("Today:"), this);
|
|
cpintTodayValue = new QLabel(tr("no data"));
|
|
QLabel *cpintAllLabel = new QLabel(tr("Best:"), this);
|
|
cpintAllValue = new QLabel(tr("no data"));
|
|
QLabel *cpintCPLabel = new QLabel(tr("CP Curve:"), this);
|
|
cpintCPValue = new QLabel(tr("no data"));
|
|
|
|
//QFontMetrics metrics(QApplication::font());
|
|
//int width = metrics.width("8888 watts (88/88/8888)") + 10;
|
|
//cpintAllValue->setFixedWidth(width);
|
|
//cpintCPValue->setFixedWidth(width); // so lines up nicely
|
|
|
|
cpintTimeValue->setReadOnly(false);
|
|
//cpintTodayValue->setReadOnly(true);
|
|
//cpintAllValue->setReadOnly(true);
|
|
//cpintCPValue->setReadOnly(true);
|
|
|
|
QFont font = cpintTimeValue->font();
|
|
font.setPointSize(font.pointSize());
|
|
cpintTodayValue->setFont(font);
|
|
cpintAllValue->setFont(font);
|
|
cpintCPValue->setFont(font);
|
|
|
|
cpintPickerLayout->addRow(cpintTimeLabel, cpintTimeValue);
|
|
cpintPickerLayout->addRow(cpintTodayLabel, cpintTodayValue);
|
|
cpintPickerLayout->addRow(cpintAllLabel, cpintAllValue);
|
|
cpintPickerLayout->addRow(cpintCPLabel, cpintCPValue);
|
|
cl->addLayout(cpintPickerLayout);
|
|
|
|
// tools /properties
|
|
seriesCombo = new QComboBox(this);
|
|
addSeries();
|
|
cComboSeason = new QComboBox(this);
|
|
seasons = parent->seasons;
|
|
resetSeasons();
|
|
if (rangemode) cComboSeason->hide();
|
|
|
|
cpintSetCPButton = new QPushButton(tr("&Save CP value"), this);
|
|
cpintSetCPButton->setEnabled(false);
|
|
cl->addWidget(cpintSetCPButton);
|
|
cl->addWidget(cComboSeason);
|
|
cl->addWidget(seriesCombo);
|
|
cl->addStretch();
|
|
|
|
picker = new QwtPlotPicker(QwtPlot::xBottom, QwtPlot::yLeft,
|
|
QwtPicker::VLineRubberBand,
|
|
QwtPicker::AlwaysOff, cpintPlot->canvas());
|
|
picker->setStateMachine(new QwtPickerDragPointMachine);
|
|
picker->setRubberBandPen(GColor(CPLOTTRACKER));
|
|
|
|
connect(picker, SIGNAL(moved(const QPoint &)), SLOT(pickerMoved(const QPoint &)));
|
|
connect(cpintTimeValue, SIGNAL(editingFinished()), this, SLOT(cpintTimeValueEntered()));
|
|
connect(cpintSetCPButton, SIGNAL(clicked()), this, SLOT(cpintSetCPButtonClicked()));
|
|
if (rangemode)
|
|
connect(this, SIGNAL(dateRangeChanged(DateRange)), SLOT(dateRangeChanged(DateRange)));
|
|
else
|
|
connect(cComboSeason, SIGNAL(currentIndexChanged(int)), this, SLOT(seasonSelected(int)));
|
|
|
|
connect(seriesCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setSeries(int)));
|
|
//connect(mainWindow, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
|
|
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
|
|
connect(mainWindow, SIGNAL(configChanged()), cpintPlot, SLOT(configChanged()));
|
|
|
|
// redraw on config change -- this seems the simplest approach
|
|
connect(mainWindow, SIGNAL(configChanged()), this, SLOT(rideSelected()));
|
|
connect(mainWindow, SIGNAL(rideAdded(RideItem*)), this, SLOT(newRideAdded(RideItem*)));
|
|
connect(mainWindow, SIGNAL(rideDeleted(RideItem*)), this, SLOT(newRideAdded(RideItem*)));
|
|
connect(seasons, SIGNAL(seasonsChanged()), this, SLOT(resetSeasons()));
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::newRideAdded(RideItem *here)
|
|
{
|
|
// any plots we already have are now stale
|
|
stale = true;
|
|
|
|
// mine just got Zapped, a new rideitem would not be my current item
|
|
if (here == currentRide) currentRide = NULL;
|
|
|
|
Season season = seasons->seasons.at(cComboSeason->currentIndex());
|
|
|
|
// Refresh global curve if a ride is added during those dates
|
|
if ((here->dateTime.date() >= season.getStart() || season.getStart() == QDate())
|
|
&& (here->dateTime.date() <= season.getEnd() || season.getEnd() == QDate()))
|
|
cpintPlot->changeSeason(season.getStart(), season.getEnd());
|
|
|
|
// if visible make the changes visible
|
|
// rideSelected is easiest way
|
|
if (amVisible()) rideSelected();
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::rideSelected()
|
|
{
|
|
if (!amVisible())
|
|
return;
|
|
currentRide = myRideItem;
|
|
if (currentRide) {
|
|
cpintPlot->calculate(currentRide);
|
|
|
|
// apply latest colors
|
|
picker->setRubberBandPen(GColor(CPLOTTRACKER));
|
|
cpintSetCPButton->setEnabled(cpintPlot->cp > 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::setSeries(int index)
|
|
{
|
|
if (index >= 0) {
|
|
cpintPlot->setSeries(static_cast<RideFile::SeriesType>(seriesCombo->itemData(index).toInt()));
|
|
cpintPlot->calculate(currentRide);
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::cpintSetCPButtonClicked()
|
|
{
|
|
int cp = (int) cpintPlot->cp;
|
|
if (cp <= 0) {
|
|
QMessageBox::critical(
|
|
this,
|
|
tr("Set CP value to extracted value"),
|
|
tr("No non-zero extracted value was identified:\n") +
|
|
tr("Zones were unchanged."));
|
|
return;
|
|
}
|
|
mainWindow->setCriticalPower(cp);
|
|
}
|
|
|
|
static double
|
|
curve_to_point(double x, const QwtPlotCurve *curve, RideFile::SeriesType serie)
|
|
{
|
|
double result = 0;
|
|
if (curve) {
|
|
const QwtSeriesData<QPointF> *data = curve->data();
|
|
|
|
if (data->size() > 0) {
|
|
if (x < data->sample(0).x() || x > data->sample(data->size() - 1).x())
|
|
return 0;
|
|
unsigned min = 0, mid = 0, max = data->size();
|
|
while (min < max - 1) {
|
|
mid = (max - min) / 2 + min;
|
|
if (x < data->sample(mid).x()) {
|
|
double a = pow(10,RideFileCache::decimalsFor(serie));
|
|
|
|
result = ((int)((0.5/a + data->sample(mid).y()) * a))/a;
|
|
//result = (unsigned) round(data->sample(mid).y());
|
|
max = mid;
|
|
}
|
|
else {
|
|
min = mid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::updateCpint(double minutes)
|
|
{
|
|
QString units;
|
|
|
|
switch (series()) {
|
|
|
|
case RideFile::none:
|
|
units = "kJ";
|
|
break;
|
|
|
|
case RideFile::cad:
|
|
units = "rpm";
|
|
break;
|
|
|
|
case RideFile::kph:
|
|
units = "kph";
|
|
break;
|
|
|
|
case RideFile::hr:
|
|
units = "bpm";
|
|
break;
|
|
|
|
case RideFile::nm:
|
|
units = "nm";
|
|
break;
|
|
|
|
case RideFile::vam:
|
|
units = "metres/hour";
|
|
break;
|
|
|
|
case RideFile::wattsKg:
|
|
units = "Watts/kg";
|
|
break;
|
|
|
|
default:
|
|
case RideFile::watts:
|
|
units = "Watts";
|
|
break;
|
|
|
|
}
|
|
|
|
// current ride
|
|
{
|
|
double value = curve_to_point(minutes, cpintPlot->getThisCurve(), series());
|
|
QString label;
|
|
if (value > 0)
|
|
label = QString("%1 %2").arg(value).arg(units);
|
|
else
|
|
label = tr("no data");
|
|
cpintTodayValue->setText(label);
|
|
}
|
|
|
|
// cp line
|
|
if (cpintPlot->getCPCurve()) {
|
|
double value = curve_to_point(minutes, cpintPlot->getCPCurve(), series());
|
|
QString label;
|
|
if (value > 0)
|
|
label = QString("%1 %2").arg(value).arg(units);
|
|
else
|
|
label = tr("no data");
|
|
cpintCPValue->setText(label);
|
|
}
|
|
|
|
// global ride
|
|
{
|
|
QString label;
|
|
int index = (int) ceil(minutes * 60);
|
|
if (index >= 0 && cpintPlot->getBests().count() > index) {
|
|
QDate date = cpintPlot->getBestDates()[index];
|
|
double value = cpintPlot->getBests()[index];
|
|
|
|
double a = pow(10,RideFileCache::decimalsFor(series()));
|
|
value = ((int)((0.5/a + value) * a))/a;
|
|
|
|
#if 0
|
|
label = QString("%1 kJ (%2)").arg(watts * minutes * 60.0 / 1000.0, 0, 'f', 0);
|
|
#endif
|
|
label = QString("%1 %2 (%3)").arg(value).arg(units)
|
|
.arg(date.isValid() ? date.toString(tr("MM/dd/yyyy")) : tr("no date"));
|
|
}
|
|
else {
|
|
label = tr("no data");
|
|
}
|
|
cpintAllValue->setText(label);
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::cpintTimeValueEntered()
|
|
{
|
|
double minutes = str_to_interval(cpintTimeValue->text()) / 60.0;
|
|
updateCpint(minutes);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::pickerMoved(const QPoint &pos)
|
|
{
|
|
double minutes = cpintPlot->invTransform(QwtPlot::xBottom, pos.x());
|
|
cpintTimeValue->setText(interval_to_str(60.0*minutes));
|
|
updateCpint(minutes);
|
|
}
|
|
void CriticalPowerWindow::addSeries()
|
|
{
|
|
// setup series list
|
|
seriesList << RideFile::watts
|
|
<< RideFile::wattsKg
|
|
<< RideFile::xPower
|
|
<< RideFile::NP
|
|
<< RideFile::hr
|
|
<< RideFile::kph
|
|
<< RideFile::cad
|
|
<< RideFile::nm
|
|
<< RideFile::vam
|
|
<< RideFile::none; // XXX actually this shows energy (hack)
|
|
|
|
foreach (RideFile::SeriesType x, seriesList) {
|
|
if (x==RideFile::none)
|
|
seriesCombo->addItem(tr("Energy"), static_cast<int>(x));
|
|
else
|
|
seriesCombo->addItem(RideFile::seriesName(x), static_cast<int>(x));
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Seasons stuff
|
|
*--------------------------------------------------------------------*/
|
|
|
|
|
|
void
|
|
CriticalPowerWindow::resetSeasons()
|
|
{
|
|
if (rangemode) return;
|
|
|
|
QString prev = cComboSeason->itemText(cComboSeason->currentIndex());
|
|
|
|
// remove seasons
|
|
cComboSeason->clear();
|
|
|
|
//Store current selection
|
|
QString previousDateRange = _dateRange;
|
|
// insert seasons
|
|
for (int i=0; i <seasons->seasons.count(); i++) {
|
|
Season season = seasons->seasons.at(i);
|
|
cComboSeason->addItem(season.getName());
|
|
}
|
|
// restore previous selection
|
|
int index = cComboSeason->findText(prev);
|
|
if (index != -1) cComboSeason->setCurrentIndex(index);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::dateRangeChanged(DateRange dateRange)
|
|
{
|
|
if (!amVisible()) return;
|
|
|
|
if (dateRange.from == cfrom && dateRange.to == cto && !stale) return;
|
|
|
|
cpintPlot->changeSeason(dateRange.from, dateRange.to);
|
|
cpintPlot->calculate(currentRide);
|
|
|
|
cfrom = dateRange.from;
|
|
cto = dateRange.to;
|
|
stale = false;
|
|
}
|
|
|
|
void CriticalPowerWindow::seasonSelected(int iSeason)
|
|
{
|
|
if (iSeason >= seasons->seasons.count() || iSeason < 0) return;
|
|
Season season = seasons->seasons.at(iSeason);
|
|
_dateRange = season.id();
|
|
cpintPlot->changeSeason(season.getStart(), season.getEnd());
|
|
cpintPlot->calculate(currentRide);
|
|
}
|
|
|
|
void CriticalPowerWindow::filterChanged()
|
|
{
|
|
cpintPlot->calculate(currentRide);
|
|
}
|