mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
Slowly migrating code and data from the MainWindow class to Athlete and Context classes. This update moves the ride and interval lists and data structures from MainWindow to Athlete.
566 lines
17 KiB
C++
566 lines
17 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 "MetricAggregator.h"
|
|
#include "CpintPlot.h"
|
|
#include "Context.h"
|
|
#include "Context.h"
|
|
#include "Athlete.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 "Zones.h"
|
|
#include <QXmlInputSource>
|
|
#include <QXmlSimpleReader>
|
|
|
|
CriticalPowerWindow::CriticalPowerWindow(const QDir &home, Context *context, bool rangemode) :
|
|
GcChartWindow(context), _dateRange("{00000000-0000-0000-0000-000000000001}"), home(home), context(context), currentRide(NULL), rangemode(rangemode), isfiltered(false), stale(true), useCustom(false), useToToday(false)
|
|
{
|
|
setInstanceName("Critical Power Window");
|
|
|
|
//
|
|
// reveal controls widget
|
|
//
|
|
|
|
// layout reveal controls
|
|
QHBoxLayout *revealLayout = new QHBoxLayout;
|
|
revealLayout->setContentsMargins(0,0,0,0);
|
|
revealLayout->addStretch();
|
|
|
|
setRevealLayout(revealLayout);
|
|
|
|
//
|
|
// main plot area
|
|
//
|
|
QVBoxLayout *vlayout = new QVBoxLayout;
|
|
cpintPlot = new CpintPlot(context, home.path(), context->athlete->zones());
|
|
vlayout->addWidget(cpintPlot);
|
|
|
|
QGridLayout *mainLayout = new QGridLayout();
|
|
mainLayout->addLayout(vlayout, 0, 0);
|
|
setChartLayout(mainLayout);
|
|
|
|
|
|
//
|
|
// picker - on chart controls/display
|
|
//
|
|
|
|
// picker widget
|
|
QWidget *pickerControls = new QWidget(this);
|
|
mainLayout->addWidget(pickerControls, 0, 0, Qt::AlignTop | Qt::AlignRight);
|
|
pickerControls->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
|
|
|
// picker layout
|
|
QVBoxLayout *pickerLayout = new QVBoxLayout(pickerControls);
|
|
QFormLayout *pcl = new QFormLayout;
|
|
pickerLayout->addLayout(pcl);
|
|
pickerLayout->addStretch(); // get labels at top right
|
|
|
|
// picker details
|
|
QLabel *cpintTimeLabel = new QLabel(tr("Duration:"), this);
|
|
cpintTimeValue = new QLabel("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"));
|
|
|
|
// chart overlayed values in smaller font
|
|
QFont font = cpintTimeValue->font();
|
|
font.setPointSize(font.pointSize()-2);
|
|
cpintTodayValue->setFont(font);
|
|
cpintAllValue->setFont(font);
|
|
cpintCPValue->setFont(font);
|
|
cpintTimeValue->setFont(font);
|
|
cpintTimeLabel->setFont(font);
|
|
cpintTodayLabel->setFont(font);
|
|
cpintAllLabel->setFont(font);
|
|
cpintCPLabel->setFont(font);
|
|
|
|
pcl->addRow(cpintTimeLabel, cpintTimeValue);
|
|
if (rangemode) {
|
|
cpintTodayLabel->hide();
|
|
cpintTodayValue->hide();
|
|
} else {
|
|
pcl->addRow(cpintTodayLabel, cpintTodayValue);
|
|
}
|
|
pcl->addRow(cpintAllLabel, cpintAllValue);
|
|
pcl->addRow(cpintCPLabel, cpintCPValue);
|
|
|
|
|
|
//
|
|
// Chart settings
|
|
//
|
|
|
|
// controls widget and layout
|
|
QWidget *c = new QWidget;
|
|
QFormLayout *cl = new QFormLayout(c);
|
|
setControls(c);
|
|
|
|
#ifdef GC_HAVE_LUCENE
|
|
// filter / searchbox
|
|
searchBox = new SearchFilterBox(this, context);
|
|
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->addRow(new QLabel(tr("Filter")), searchBox);
|
|
cl->addWidget(new QLabel("")); //spacing
|
|
#endif
|
|
|
|
// series
|
|
seriesCombo = new QComboBox(this);
|
|
addSeries();
|
|
|
|
// data -- season / daterange edit
|
|
cComboSeason = new QComboBox(this);
|
|
seasons = context->athlete->seasons;
|
|
resetSeasons();
|
|
QLabel *label = new QLabel(tr("Date range"));
|
|
QLabel *label2 = new QLabel(tr("Date range"));
|
|
if (rangemode) {
|
|
cComboSeason->hide();
|
|
label2->hide();
|
|
}
|
|
cl->addRow(label2, cComboSeason);
|
|
dateSetting = new DateSettingsEdit(this);
|
|
cl->addRow(label, dateSetting);
|
|
if (rangemode == false) {
|
|
dateSetting->hide();
|
|
label->hide();
|
|
}
|
|
|
|
cl->addWidget(new QLabel("")); //spacing
|
|
cl->addRow(new QLabel(tr("Data series")), seriesCombo);
|
|
|
|
// shading
|
|
shadeCombo = new QComboBox(this);
|
|
shadeCombo->addItem(tr("None"));
|
|
shadeCombo->addItem(tr("Using CP"));
|
|
shadeCombo->addItem(tr("Using derived CP"));
|
|
QLabel *shading = new QLabel(tr("Power Shading"));
|
|
shadeCombo->setCurrentIndex(2);
|
|
cl->addRow(shading, shadeCombo);
|
|
|
|
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 &)));
|
|
|
|
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(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
|
|
connect(context, SIGNAL(configChanged()), cpintPlot, SLOT(configChanged()));
|
|
|
|
// redraw on config change -- this seems the simplest approach
|
|
connect(context->mainWindow, SIGNAL(filterChanged(QStringList&)), this, SLOT(forceReplot()));
|
|
connect(context, SIGNAL(configChanged()), this, SLOT(rideSelected()));
|
|
connect(context->athlete->metricDB, SIGNAL(dataChanged()), this, SLOT(refreshRideSaved()));
|
|
connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(newRideAdded(RideItem*)));
|
|
connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(newRideAdded(RideItem*)));
|
|
connect(seasons, SIGNAL(seasonsChanged()), this, SLOT(resetSeasons()));
|
|
connect(shadeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(shadingSelected(int)));
|
|
connect(dateSetting, SIGNAL(useCustomRange(DateRange)), this, SLOT(useCustomRange(DateRange)));
|
|
connect(dateSetting, SIGNAL(useThruToday()), this, SLOT(useThruToday()));
|
|
connect(dateSetting, SIGNAL(useStandardRange()), this, SLOT(useStandardRange()));
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::refreshRideSaved()
|
|
{
|
|
const RideItem *current = context->rideItem();
|
|
if (!current) return;
|
|
|
|
// if the saved ride is in the aggregated time period
|
|
QDate date = current->dateTime.date();
|
|
if (date >= cpintPlot->startDate &&
|
|
date <= cpintPlot->endDate) {
|
|
|
|
// force a redraw next time visible
|
|
cpintPlot->changeSeason(cpintPlot->startDate, cpintPlot->endDate);
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::forceReplot()
|
|
{
|
|
stale = true; // we must become stale
|
|
if (rangemode) {
|
|
|
|
// force replot...
|
|
dateRangeChanged(myDateRange);
|
|
|
|
} else {
|
|
Season season = seasons->seasons.at(cComboSeason->currentIndex());
|
|
|
|
// Refresh aggregated curve (ride added/filter changed)
|
|
cpintPlot->changeSeason(season.getStart(), season.getEnd());
|
|
|
|
// if visible make the changes visible
|
|
// rideSelected is easiest way
|
|
if (amVisible()) rideSelected();
|
|
}
|
|
repaint();
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::newRideAdded(RideItem *here)
|
|
{
|
|
// mine just got Zapped, a new rideitem would not be my current item
|
|
if (here == currentRide) currentRide = NULL;
|
|
|
|
// any plots we already have are now stale
|
|
if (!rangemode) {
|
|
|
|
Season season = seasons->seasons.at(cComboSeason->currentIndex());
|
|
stale = true;
|
|
|
|
if ((here->dateTime.date() >= season.getStart() || season.getStart() == QDate())
|
|
&& (here->dateTime.date() <= season.getEnd() || season.getEnd() == QDate())) {
|
|
// replot
|
|
forceReplot();
|
|
}
|
|
|
|
} else {
|
|
|
|
forceReplot();
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::rideSelected()
|
|
{
|
|
if (!amVisible()) return;
|
|
|
|
currentRide = myRideItem;
|
|
if (currentRide) {
|
|
if (context->athlete->zones()) {
|
|
int zoneRange = context->athlete->zones()->whichRange(currentRide->dateTime.date());
|
|
int CP = zoneRange >= 0 ? context->athlete->zones()->getCP(zoneRange) : 0;
|
|
cpintPlot->setDateCP(CP);
|
|
} else {
|
|
cpintPlot->setDateCP(0);
|
|
}
|
|
cpintPlot->calculate(currentRide);
|
|
|
|
// apply latest colors
|
|
picker->setRubberBandPen(GColor(CPLOTTRACKER));
|
|
setIsBlank(false);
|
|
} else if (!rangemode) {
|
|
setIsBlank(true);
|
|
}
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::setSeries(int index)
|
|
{
|
|
if (index >= 0) {
|
|
cpintPlot->setSeries(static_cast<RideFile::SeriesType>(seriesCombo->itemData(index).toInt()));
|
|
cpintPlot->calculate(currentRide);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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; // 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::useCustomRange(DateRange range)
|
|
{
|
|
// plot using the supplied range
|
|
useCustom = true;
|
|
useToToday = false;
|
|
custom = range;
|
|
dateRangeChanged(custom);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::useStandardRange()
|
|
{
|
|
useToToday = useCustom = false;
|
|
dateRangeChanged(myDateRange);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::useThruToday()
|
|
{
|
|
// plot using the supplied range
|
|
useCustom = false;
|
|
useToToday = true;
|
|
custom = myDateRange;
|
|
if (custom.to > QDate::currentDate()) custom.to = QDate::currentDate();
|
|
dateRangeChanged(custom);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::dateRangeChanged(DateRange dateRange)
|
|
{
|
|
if (!amVisible()) return;
|
|
|
|
// it will either be sidebar or custom...
|
|
if (useCustom) dateRange = custom;
|
|
else if (useToToday) {
|
|
|
|
dateRange = myDateRange;
|
|
QDate today = QDate::currentDate();
|
|
if (dateRange.to > today) dateRange.to = today;
|
|
|
|
} else dateRange = myDateRange;
|
|
|
|
if (dateRange.from == cfrom && dateRange.to == cto && !stale) return;
|
|
|
|
cfrom = dateRange.from;
|
|
cto = dateRange.to;
|
|
|
|
// lets work out the average CP configure value
|
|
if (context->athlete->zones()) {
|
|
int fromZoneRange = context->athlete->zones()->whichRange(cfrom);
|
|
int toZoneRange = context->athlete->zones()->whichRange(cto);
|
|
|
|
int CPfrom = fromZoneRange >= 0 ? context->athlete->zones()->getCP(fromZoneRange) : 0;
|
|
int CPto = toZoneRange >= 0 ? context->athlete->zones()->getCP(toZoneRange) : CPfrom;
|
|
if (CPfrom == 0) CPfrom = CPto;
|
|
int dateCP = (CPfrom + CPto) / 2;
|
|
|
|
cpintPlot->setDateCP(dateCP);
|
|
}
|
|
|
|
cpintPlot->changeSeason(dateRange.from, dateRange.to);
|
|
cpintPlot->calculate(currentRide);
|
|
|
|
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);
|
|
}
|
|
|
|
void
|
|
CriticalPowerWindow::shadingSelected(int shading)
|
|
{
|
|
cpintPlot->setShadeMode(shading);
|
|
if (rangemode) dateRangeChanged(DateRange());
|
|
else cpintPlot->calculate(currentRide);
|
|
}
|