mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
1917 lines
60 KiB
C++
1917 lines
60 KiB
C++
/*
|
|
* Copyright (c) 2006 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 "MainWindow.h"
|
|
#include "AllPlot.h"
|
|
#include "BestIntervalDialog.h"
|
|
#include "ChooseCyclistDialog.h"
|
|
#include "ConfigDialog.h"
|
|
#include "CpintPlot.h"
|
|
#include "PfPvPlot.h"
|
|
#include "DownloadRideDialog.h"
|
|
#include "ManualRideDialog.h"
|
|
#include "PowerHist.h"
|
|
#include "RideItem.h"
|
|
#include "RideFile.h"
|
|
#include "RideImportWizard.h"
|
|
#include "QuarqRideFile.h"
|
|
#include "RideMetric.h"
|
|
#include "Settings.h"
|
|
#include "TimeUtils.h"
|
|
#include "Zones.h"
|
|
#include <assert.h>
|
|
#include <QApplication>
|
|
#include <QtGui>
|
|
#include <QRegExp>
|
|
#include <qwt_plot_curve.h>
|
|
#include <qwt_plot_panner.h>
|
|
#include <qwt_plot_picker.h>
|
|
#include <qwt_plot_zoomer.h>
|
|
#include <qwt_plot_grid.h>
|
|
#include <qwt_data.h>
|
|
#include <boost/scoped_ptr.hpp>
|
|
#include "DaysScaleDraw.h"
|
|
#include "RideCalendar.h"
|
|
#include "DatePickerDialog.h"
|
|
#include "ToolsDialog.h"
|
|
#include "MetricAggregator.h"
|
|
#include "SplitRideDialog.h"
|
|
|
|
#ifndef GC_VERSION
|
|
#define GC_VERSION "(developer build)"
|
|
#endif
|
|
|
|
#define FOLDER_TYPE 0
|
|
#define RIDE_TYPE 1
|
|
|
|
bool
|
|
MainWindow::parseRideFileName(const QString &name, QString *notesFileName, QDateTime *dt)
|
|
{
|
|
static char rideFileRegExp[] = "^((\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)"
|
|
"_(\\d\\d)_(\\d\\d)_(\\d\\d))\\.(.+)$";
|
|
QRegExp rx(rideFileRegExp);
|
|
if (!rx.exactMatch(name))
|
|
return false;
|
|
assert(rx.numCaptures() == 8);
|
|
QDate date(rx.cap(2).toInt(), rx.cap(3).toInt(),rx.cap(4).toInt());
|
|
QTime time(rx.cap(5).toInt(), rx.cap(6).toInt(),rx.cap(7).toInt());
|
|
if ((! date.isValid()) || (! time.isValid())) {
|
|
QMessageBox::warning(this,
|
|
tr("Invalid Ride File Name"),
|
|
tr("Invalid date/time in filename:\n%1\nSkipping file...").arg(name)
|
|
);
|
|
return false;
|
|
}
|
|
*dt = QDateTime(date, time);
|
|
*notesFileName = rx.cap(1) + ".notes";
|
|
return true;
|
|
}
|
|
|
|
MainWindow::MainWindow(const QDir &home) :
|
|
home(home),
|
|
zones(NULL), currentNotesChanged(false),
|
|
ride(NULL)
|
|
{
|
|
settings = GetApplicationSettings();
|
|
|
|
QVariant unit = settings->value(GC_UNIT);
|
|
useMetricUnits = (unit.toString() == "Metric");
|
|
|
|
setWindowTitle(home.dirName());
|
|
settings->setValue(GC_SETTINGS_LAST, home.dirName());
|
|
|
|
setWindowIcon(QIcon(":images/gc.png"));
|
|
setAcceptDrops(true);
|
|
|
|
QFile zonesFile(home.absolutePath() + "/power.zones");
|
|
if (zonesFile.exists()) {
|
|
zones = new Zones();
|
|
if (!zones->read(zonesFile)) {
|
|
QMessageBox::critical(this, tr("Zones File Error"),
|
|
zones->errorString());
|
|
delete zones;
|
|
zones = NULL;
|
|
}
|
|
else if (! zones->warningString().isEmpty())
|
|
QMessageBox::warning(this, tr("Reading Zones File"), zones->warningString());
|
|
}
|
|
|
|
QVariant geom = settings->value(GC_SETTINGS_MAIN_GEOM);
|
|
if (geom == QVariant())
|
|
resize(640, 480);
|
|
else
|
|
setGeometry(geom.toRect());
|
|
|
|
splitter = new QSplitter(this);
|
|
setCentralWidget(splitter);
|
|
splitter->setContentsMargins(10, 20, 10, 10); // attempting to follow some UI guides
|
|
|
|
calendar = new RideCalendar;
|
|
calendar->setFirstDayOfWeek(Qt::Monday);
|
|
calendar->setHome(home);
|
|
calendar->addWorkoutCode(QString("race"), QColor(Qt::red));
|
|
calendar->addWorkoutCode(QString("sick"), QColor(Qt::yellow));
|
|
calendar->addWorkoutCode(QString("swim"), QColor(Qt::blue));
|
|
calendar->addWorkoutCode(QString("gym"), QColor(Qt::gray));
|
|
|
|
treeWidget = new QTreeWidget;
|
|
treeWidget->setColumnCount(3);
|
|
treeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
// TODO: Test this on various systems with differing font settings (looks good on Leopard :)
|
|
treeWidget->header()->resizeSection(0,70);
|
|
treeWidget->header()->resizeSection(1,95);
|
|
treeWidget->header()->resizeSection(2,70);
|
|
//treeWidget->setMaximumWidth(250);
|
|
treeWidget->header()->hide();
|
|
treeWidget->setAlternatingRowColors (true);
|
|
treeWidget->setIndentation(5);
|
|
|
|
allRides = new QTreeWidgetItem(treeWidget, FOLDER_TYPE);
|
|
allRides->setText(0, tr("All Rides"));
|
|
treeWidget->expandItem(allRides);
|
|
|
|
leftLayout = new QSplitter;
|
|
leftLayout->setOrientation(Qt::Vertical);
|
|
leftLayout->addWidget(calendar);
|
|
leftLayout->setCollapsible(0, false);
|
|
leftLayout->addWidget(treeWidget);
|
|
leftLayout->setCollapsible(1, false);
|
|
splitter->addWidget(leftLayout);
|
|
splitter->setCollapsible(0, true);
|
|
QVariant calendarSizes = settings->value(GC_SETTINGS_CALENDAR_SIZES);
|
|
if (calendarSizes != QVariant()) {
|
|
leftLayout->restoreState(calendarSizes.toByteArray());
|
|
}
|
|
|
|
QTreeWidgetItem *last = NULL;
|
|
QStringListIterator i(RideFileFactory::instance().listRideFiles(home));
|
|
while (i.hasNext()) {
|
|
QString name = i.next(), notesFileName;
|
|
QDateTime dt;
|
|
if (parseRideFileName(name, ¬esFileName, &dt)) {
|
|
last = new RideItem(RIDE_TYPE, home.path(),
|
|
name, dt, &zones, notesFileName);
|
|
allRides->addChild(last);
|
|
calendar->addRide(reinterpret_cast<RideItem*>(last));
|
|
}
|
|
}
|
|
|
|
tabWidget = new QTabWidget;
|
|
tabWidget->setUsesScrollButtons(true);
|
|
rideSummary = new QTextEdit;
|
|
rideSummary->setReadOnly(true);
|
|
tabWidget->addTab(rideSummary, tr("Ride Summary"));
|
|
|
|
/////////////////////////// Ride Plot Tab ///////////////////////////
|
|
QWidget *window = new QWidget;
|
|
QVBoxLayout *vlayout = new QVBoxLayout;
|
|
|
|
QHBoxLayout *showLayout = new QHBoxLayout;
|
|
QLabel *showLabel = new QLabel("Show:", window);
|
|
showLayout->addWidget(showLabel);
|
|
|
|
QCheckBox *showGrid = new QCheckBox("Grid", window);
|
|
showGrid->setCheckState(Qt::Checked);
|
|
showLayout->addWidget(showGrid);
|
|
|
|
showHr = new QCheckBox("Heart Rate", window);
|
|
showHr->setCheckState(Qt::Checked);
|
|
showLayout->addWidget(showHr);
|
|
|
|
showSpeed = new QCheckBox("Speed", window);
|
|
showSpeed->setCheckState(Qt::Checked);
|
|
showLayout->addWidget(showSpeed);
|
|
|
|
showCad = new QCheckBox("Cadence", window);
|
|
showCad->setCheckState(Qt::Checked);
|
|
showLayout->addWidget(showCad);
|
|
|
|
showAlt = new QCheckBox("Altitude", window);
|
|
showAlt->setCheckState(Qt::Checked);
|
|
showLayout->addWidget(showAlt);
|
|
|
|
showPower = new QComboBox();
|
|
showPower->addItem(tr("Power + shade"));
|
|
showPower->addItem(tr("Power - shade"));
|
|
showPower->addItem(tr("No Power"));
|
|
showLayout->addWidget(showPower);
|
|
|
|
QHBoxLayout *smoothLayout = new QHBoxLayout;
|
|
QComboBox *comboDistance = new QComboBox();
|
|
comboDistance->addItem(tr("X Axis Shows Time"));
|
|
comboDistance->addItem(tr("X Axis Shows Distance"));
|
|
smoothLayout->addWidget(comboDistance);
|
|
|
|
QLabel *smoothLabel = new QLabel(tr("Smoothing (secs)"), window);
|
|
smoothLineEdit = new QLineEdit(window);
|
|
smoothLineEdit->setFixedWidth(40);
|
|
|
|
smoothLayout->addWidget(smoothLabel);
|
|
smoothLayout->addWidget(smoothLineEdit);
|
|
smoothSlider = new QSlider(Qt::Horizontal);
|
|
smoothSlider->setTickPosition(QSlider::TicksBelow);
|
|
smoothSlider->setTickInterval(10);
|
|
smoothSlider->setMinimum(2);
|
|
smoothSlider->setMaximum(600);
|
|
smoothLineEdit->setValidator(new QIntValidator(smoothSlider->minimum(),
|
|
smoothSlider->maximum(),
|
|
smoothLineEdit));
|
|
smoothLayout->addWidget(smoothSlider);
|
|
allPlot = new AllPlot(this);
|
|
smoothSlider->setValue(allPlot->smoothing());
|
|
smoothLineEdit->setText(QString("%1").arg(allPlot->smoothing()));
|
|
|
|
allZoomer = new QwtPlotZoomer(allPlot->canvas());
|
|
allZoomer->setRubberBand(QwtPicker::RectRubberBand);
|
|
allZoomer->setRubberBandPen(QColor(Qt::black));
|
|
allZoomer->setSelectionFlags(QwtPicker::DragSelection
|
|
| QwtPicker::CornerToCorner);
|
|
allZoomer->setTrackerMode(QwtPicker::AlwaysOff);
|
|
allZoomer->setEnabled(true);
|
|
|
|
// TODO: Hack for OS X one-button mouse
|
|
// allZoomer->initMousePattern(1);
|
|
|
|
// RightButton: zoom out by 1
|
|
// Ctrl+RightButton: zoom out to full size
|
|
allZoomer->setMousePattern(QwtEventPattern::MouseSelect2,
|
|
Qt::RightButton, Qt::ControlModifier);
|
|
allZoomer->setMousePattern(QwtEventPattern::MouseSelect3,
|
|
Qt::RightButton);
|
|
|
|
allPanner = new QwtPlotPanner(allPlot->canvas());
|
|
allPanner->setMouseButton(Qt::MidButton);
|
|
|
|
// TODO: zoomer doesn't interact well with automatic axis resizing
|
|
|
|
vlayout->addWidget(allPlot);
|
|
vlayout->addLayout(showLayout);
|
|
vlayout->addLayout(smoothLayout);
|
|
window->setLayout(vlayout);
|
|
tabWidget->addTab(window, "Ride Plot");
|
|
splitter->addWidget(tabWidget);
|
|
splitter->setCollapsible(1, true);
|
|
|
|
QVariant splitterSizes = settings->value(GC_SETTINGS_SPLITTER_SIZES);
|
|
if (splitterSizes != QVariant())
|
|
splitter->restoreState(splitterSizes.toByteArray());
|
|
else {
|
|
QList<int> sizes;
|
|
sizes.append(250);
|
|
sizes.append(390);
|
|
splitter->setSizes(sizes);
|
|
}
|
|
|
|
////////////////////// Critical Power Plot Tab //////////////////////
|
|
|
|
window = new QWidget;
|
|
vlayout = new QVBoxLayout;
|
|
QHBoxLayout *cpintPickerLayout = new QHBoxLayout;
|
|
QLabel *cpintTimeLabel = new QLabel(tr("Interval Duration:"), window);
|
|
cpintTimeValue = new QLineEdit("0 s");
|
|
QLabel *cpintTodayLabel = new QLabel(tr("Today:"), window);
|
|
cpintTodayValue = new QLineEdit(tr("no data"));
|
|
QLabel *cpintAllLabel = new QLabel(tr("All Rides:"), window);
|
|
cpintAllValue = new QLineEdit(tr("no data"));
|
|
cpintTimeValue->setReadOnly(true);
|
|
cpintTodayValue->setReadOnly(true);
|
|
cpintAllValue->setReadOnly(true);
|
|
|
|
cpintSetCPButton = new QPushButton(tr("&Save CP value"), this);
|
|
cpintSetCPButton->setEnabled(false);
|
|
|
|
cpintPickerLayout->addWidget(cpintTimeLabel);
|
|
cpintPickerLayout->addWidget(cpintTimeValue);
|
|
cpintPickerLayout->addWidget(cpintTodayLabel);
|
|
cpintPickerLayout->addWidget(cpintTodayValue);
|
|
cpintPickerLayout->addWidget(cpintAllLabel);
|
|
cpintPickerLayout->addWidget(cpintAllValue);
|
|
cpintPickerLayout->addWidget(cpintSetCPButton);
|
|
cpintPlot = new CpintPlot(home.path());
|
|
vlayout->addWidget(cpintPlot);
|
|
vlayout->addLayout(cpintPickerLayout);
|
|
window->setLayout(vlayout);
|
|
tabWidget->addTab(window, "Critical Power Plot");
|
|
|
|
picker = new QwtPlotPicker(QwtPlot::xBottom, QwtPlot::yLeft,
|
|
QwtPicker::PointSelection,
|
|
QwtPicker::VLineRubberBand,
|
|
QwtPicker::AlwaysOff, cpintPlot->canvas());
|
|
picker->setRubberBandPen(QColor(Qt::blue));
|
|
|
|
connect(picker, SIGNAL(moved(const QPoint &)),
|
|
SLOT(pickerMoved(const QPoint &)));
|
|
|
|
//////////////////////// Power Histogram Tab ////////////////////////
|
|
|
|
window = new QWidget;
|
|
vlayout = new QVBoxLayout;
|
|
QHBoxLayout *binWidthLayout = new QHBoxLayout;
|
|
QLabel *binWidthLabel = new QLabel(tr("Bin width"), window);
|
|
binWidthLineEdit = new QLineEdit(window);
|
|
binWidthLineEdit->setFixedWidth(30);
|
|
|
|
binWidthLayout->addWidget(binWidthLabel);
|
|
binWidthLayout->addWidget(binWidthLineEdit);
|
|
binWidthSlider = new QSlider(Qt::Horizontal);
|
|
binWidthSlider->setTickPosition(QSlider::TicksBelow);
|
|
binWidthSlider->setTickInterval(1);
|
|
binWidthSlider->setMinimum(1);
|
|
binWidthSlider->setMaximum(100);
|
|
binWidthLayout->addWidget(binWidthSlider);
|
|
|
|
lnYHistCheckBox = new QCheckBox;
|
|
lnYHistCheckBox->setText("Log y");
|
|
binWidthLayout->addWidget(lnYHistCheckBox);
|
|
|
|
withZerosCheckBox = new QCheckBox;
|
|
withZerosCheckBox->setText("With zeros");
|
|
binWidthLayout->addWidget(withZerosCheckBox);
|
|
histParameterCombo = new QComboBox();
|
|
binWidthLayout->addWidget(histParameterCombo);
|
|
|
|
powerHist = new PowerHist();
|
|
setHistTextValidator();
|
|
|
|
lnYHistCheckBox->setChecked(powerHist->islnY());
|
|
withZerosCheckBox->setChecked(powerHist->withZeros());
|
|
binWidthSlider->setValue(powerHist->binWidth());
|
|
setHistBinWidthText();
|
|
vlayout->addWidget(powerHist);
|
|
vlayout->addLayout(binWidthLayout);
|
|
window->setLayout(vlayout);
|
|
tabWidget->addTab(window, "Histogram Analysis");
|
|
|
|
//////////////////////// Pedal Force/Velocity Plot ////////////////////////
|
|
|
|
window = new QWidget;
|
|
vlayout = new QVBoxLayout;
|
|
QHBoxLayout *qaLayout = new QHBoxLayout;
|
|
|
|
pfPvPlot = new PfPvPlot();
|
|
QLabel *qaCPLabel = new QLabel(tr("Watts:"), window);
|
|
qaCPValue = new QLineEdit(QString("%1").arg(pfPvPlot->getCP()));
|
|
qaCPValue->setValidator(new QIntValidator(0, 9999, qaCPValue));
|
|
QLabel *qaCadLabel = new QLabel(tr("RPM:"), window);
|
|
qaCadValue = new QLineEdit(QString("%1").arg(pfPvPlot->getCAD()));
|
|
qaCadValue->setValidator(new QIntValidator(0, 999, qaCadValue));
|
|
QLabel *qaClLabel = new QLabel(tr("Crank Length (m):"), window);
|
|
qaClValue = new QLineEdit(QString("%1").arg(1000 * pfPvPlot->getCL()));
|
|
shadeZonesPfPvCheckBox = new QCheckBox;
|
|
shadeZonesPfPvCheckBox->setText("Shade zones");
|
|
shadeZonesPfPvCheckBox->setCheckState(Qt::Checked);
|
|
|
|
qaLayout->addWidget(qaCPLabel);
|
|
qaLayout->addWidget(qaCPValue);
|
|
qaLayout->addWidget(qaCadLabel);
|
|
qaLayout->addWidget(qaCadValue);
|
|
qaLayout->addWidget(qaClLabel);
|
|
qaLayout->addWidget(qaClValue);
|
|
qaLayout->addWidget(shadeZonesPfPvCheckBox);
|
|
|
|
vlayout->addWidget(pfPvPlot);
|
|
vlayout->addLayout(qaLayout);
|
|
window->setLayout(vlayout);
|
|
|
|
connect(pfPvPlot, SIGNAL(changedCP(const QString&)),
|
|
qaCPValue, SLOT(setText(const QString&)) );
|
|
connect(pfPvPlot, SIGNAL(changedCAD(const QString&)),
|
|
qaCadValue, SLOT(setText(const QString&)) );
|
|
connect(pfPvPlot, SIGNAL(changedCL(const QString&)),
|
|
qaClValue, SLOT(setText(const QString&)) );
|
|
|
|
tabWidget->addTab(window, tr("PF/PV Plot"));
|
|
|
|
|
|
//////////////////////// Ride Notes ////////////////////////
|
|
|
|
rideNotes = new QTextEdit;
|
|
tabWidget->addTab(rideNotes, tr("Notes"));
|
|
|
|
//////////////////////// Weekly Summary ////////////////////////
|
|
|
|
// add daily distance / duration graph:
|
|
window = new QWidget;
|
|
QGridLayout *glayout = new QGridLayout;
|
|
|
|
// set up the weekly distance / duration plot:
|
|
weeklyPlot = new QwtPlot();
|
|
weeklyPlot->enableAxis(QwtPlot::yRight, true);
|
|
weeklyPlot->setAxisMaxMinor(QwtPlot::xBottom,0);
|
|
weeklyPlot->setAxisScaleDraw(QwtPlot::xBottom, new DaysScaleDraw());
|
|
QFont weeklyPlotAxisFont = weeklyPlot->axisFont(QwtPlot::yLeft);
|
|
weeklyPlotAxisFont.setPointSize(weeklyPlotAxisFont.pointSize() * 0.9f);
|
|
weeklyPlot->setAxisFont(QwtPlot::xBottom, weeklyPlotAxisFont);
|
|
weeklyPlot->setAxisFont(QwtPlot::yLeft, weeklyPlotAxisFont);
|
|
weeklyPlot->setAxisFont(QwtPlot::yRight, weeklyPlotAxisFont);
|
|
|
|
weeklyDistCurve = new QwtPlotCurve();
|
|
weeklyDistCurve->setStyle(QwtPlotCurve::Steps);
|
|
QPen pen(Qt::SolidLine);
|
|
weeklyDistCurve->setPen(pen);
|
|
weeklyDistCurve->setBrush(Qt::red);
|
|
weeklyDistCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
weeklyDistCurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left
|
|
weeklyDistCurve->attach(weeklyPlot);
|
|
|
|
weeklyDurationCurve = new QwtPlotCurve();
|
|
weeklyDurationCurve->setStyle(QwtPlotCurve::Steps);
|
|
weeklyDurationCurve->setPen(pen);
|
|
weeklyDurationCurve->setBrush(QColor(255,200,0,255));
|
|
weeklyDurationCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
weeklyDurationCurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left
|
|
weeklyDurationCurve->setYAxis(QwtPlot::yRight);
|
|
weeklyDurationCurve->attach(weeklyPlot);
|
|
|
|
// set up the weekly bike score plot:
|
|
weeklyBSPlot = new QwtPlot();
|
|
weeklyBSPlot->enableAxis(QwtPlot::yRight, true);
|
|
weeklyBSPlot->setAxisMaxMinor(QwtPlot::xBottom,0);
|
|
weeklyBSPlot->setAxisScaleDraw(QwtPlot::xBottom, new DaysScaleDraw());
|
|
QwtText textLabel = QwtText();
|
|
weeklyBSPlot->setAxisFont(QwtPlot::xBottom, weeklyPlotAxisFont);
|
|
weeklyBSPlot->setAxisFont(QwtPlot::yLeft, weeklyPlotAxisFont);
|
|
weeklyBSPlot->setAxisFont(QwtPlot::yRight, weeklyPlotAxisFont);
|
|
|
|
weeklyBSCurve = new QwtPlotCurve();
|
|
weeklyBSCurve->setStyle(QwtPlotCurve::Steps);
|
|
weeklyBSCurve->setPen(pen);
|
|
weeklyBSCurve->setBrush(Qt::blue);
|
|
weeklyBSCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
weeklyBSCurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left
|
|
weeklyBSCurve->attach(weeklyBSPlot);
|
|
|
|
weeklyRICurve = new QwtPlotCurve();
|
|
weeklyRICurve->setStyle(QwtPlotCurve::Steps);
|
|
weeklyRICurve->setPen(pen);
|
|
weeklyRICurve->setBrush(Qt::green);
|
|
weeklyRICurve->setRenderHint(QwtPlotItem::RenderAntialiased);
|
|
weeklyRICurve->setCurveAttribute(QwtPlotCurve::Inverted, true); // inverted, right-to-left
|
|
weeklyRICurve->setYAxis(QwtPlot::yRight);
|
|
weeklyRICurve->attach(weeklyBSPlot);
|
|
|
|
// set baseline curves to obscure linewidth variations along baseline
|
|
pen.setWidth(2);
|
|
weeklyBaselineCurve = new QwtPlotCurve();
|
|
weeklyBaselineCurve->setPen(pen);
|
|
weeklyBaselineCurve->attach(weeklyPlot);
|
|
weeklyBSBaselineCurve = new QwtPlotCurve();
|
|
weeklyBSBaselineCurve->setPen(pen);
|
|
weeklyBSBaselineCurve->attach(weeklyBSPlot);
|
|
|
|
QwtPlotGrid *grid = new QwtPlotGrid();
|
|
grid->enableX(false);
|
|
QPen gridPen;
|
|
gridPen.setStyle(Qt::DotLine);
|
|
grid->setPen(gridPen);
|
|
grid->attach(weeklyPlot);
|
|
|
|
QwtPlotGrid *grid1 = new QwtPlotGrid();
|
|
grid1->enableX(false);
|
|
gridPen.setStyle(Qt::DotLine);
|
|
grid1->setPen(gridPen);
|
|
grid1->attach(weeklyBSPlot);
|
|
|
|
weeklySummary = new QTextEdit;
|
|
weeklySummary->setReadOnly(true);
|
|
|
|
glayout->addWidget(weeklySummary,0,0,1,-1); // row, col, rowspan, colspan. -1 == fill to edge
|
|
glayout->addWidget(weeklyPlot,1,0);
|
|
glayout->addWidget(weeklyBSPlot,1,1);
|
|
|
|
glayout->setRowStretch(0, 3); // stretch factor of summary
|
|
glayout->setRowStretch(1, 2); // stretch factor of weekly plots
|
|
glayout->setColumnStretch(0, 1); // stretch first column
|
|
glayout->setColumnStretch(1, 1); // stretch second column
|
|
glayout->setRowMinimumHeight(0, 180); // minimum height of weekly summary
|
|
glayout->setRowMinimumHeight(1, 120); // minimum height of weekly plots
|
|
|
|
window->setLayout(glayout);
|
|
tabWidget->addTab(window, tr("Weekly Summary"));
|
|
|
|
////////////////////////////// Signals //////////////////////////////
|
|
|
|
connect(calendar, SIGNAL(clicked(const QDate &)),
|
|
this, SLOT(dateChanged(const QDate &)));
|
|
connect(leftLayout, SIGNAL(splitterMoved(int,int)),
|
|
this, SLOT(leftLayoutMoved()));
|
|
connect(treeWidget, SIGNAL(itemSelectionChanged()),
|
|
this, SLOT(rideSelected()));
|
|
connect(splitter, SIGNAL(splitterMoved(int,int)),
|
|
this, SLOT(splitterMoved()));
|
|
connect(showPower, SIGNAL(currentIndexChanged(int)),
|
|
allPlot, SLOT(showPower(int)));
|
|
connect(showHr, SIGNAL(stateChanged(int)),
|
|
allPlot, SLOT(showHr(int)));
|
|
connect(showSpeed, SIGNAL(stateChanged(int)),
|
|
allPlot, SLOT(showSpeed(int)));
|
|
connect(showCad, SIGNAL(stateChanged(int)),
|
|
allPlot, SLOT(showCad(int)));
|
|
connect(showAlt, SIGNAL(stateChanged(int)),
|
|
allPlot, SLOT(showAlt(int)));
|
|
connect(showGrid, SIGNAL(stateChanged(int)),
|
|
allPlot, SLOT(showGrid(int)));
|
|
connect(comboDistance, SIGNAL(currentIndexChanged(int)),
|
|
allPlot, SLOT(setByDistance(int)));
|
|
connect(smoothSlider, SIGNAL(valueChanged(int)),
|
|
this, SLOT(setSmoothingFromSlider()));
|
|
connect(smoothLineEdit, SIGNAL(editingFinished()),
|
|
this, SLOT(setSmoothingFromLineEdit()));
|
|
connect(cpintSetCPButton, SIGNAL(clicked()),
|
|
this, SLOT(cpintSetCPButtonClicked()));
|
|
connect(binWidthSlider, SIGNAL(valueChanged(int)),
|
|
this, SLOT(setBinWidthFromSlider()));
|
|
connect(binWidthLineEdit, SIGNAL(editingFinished()),
|
|
this, SLOT(setBinWidthFromLineEdit()));
|
|
connect(lnYHistCheckBox, SIGNAL(stateChanged(int)),
|
|
this, SLOT(setlnYHistFromCheckBox()));
|
|
connect(withZerosCheckBox, SIGNAL(stateChanged(int)),
|
|
this, SLOT(setWithZerosFromCheckBox()));
|
|
connect(histParameterCombo, SIGNAL(currentIndexChanged(int)),
|
|
this, SLOT(setHistSelection(int)));
|
|
connect(shadeZonesPfPvCheckBox, SIGNAL(stateChanged(int)),
|
|
this, SLOT(setShadeZonesPfPvFromCheckBox()));
|
|
connect(qaCPValue, SIGNAL(editingFinished()),
|
|
this, SLOT(setQaCPFromLineEdit()));
|
|
connect(qaCadValue, SIGNAL(editingFinished()),
|
|
this, SLOT(setQaCADFromLineEdit()));
|
|
connect(qaClValue, SIGNAL(editingFinished()),
|
|
this, SLOT(setQaCLFromLineEdit()));
|
|
connect(tabWidget, SIGNAL(currentChanged(int)),
|
|
this, SLOT(tabChanged(int)));
|
|
connect(rideNotes, SIGNAL(textChanged()),
|
|
this, SLOT(notesChanged()));
|
|
|
|
/////////////////////////////// Menus ///////////////////////////////
|
|
|
|
QMenu *fileMenu = menuBar()->addMenu(tr("&Cyclist"));
|
|
fileMenu->addAction(tr("&New..."), this,
|
|
SLOT(newCyclist()), tr("Ctrl+N"));
|
|
fileMenu->addAction(tr("&Open..."), this,
|
|
SLOT(openCyclist()), tr("Ctrl+O"));
|
|
fileMenu->addAction(tr("&Quit"), this,
|
|
SLOT(close()), tr("Ctrl+Q"));
|
|
|
|
QMenu *rideMenu = menuBar()->addMenu(tr("&Ride"));
|
|
rideMenu->addAction(tr("&Download from device..."), this,
|
|
SLOT(downloadRide()), tr("Ctrl+D"));
|
|
rideMenu->addAction(tr("&Export to CSV..."), this,
|
|
SLOT(exportCSV()), tr("Ctrl+E"));
|
|
rideMenu->addAction(tr("&Export to XML..."), this,
|
|
SLOT(exportXML()));
|
|
rideMenu->addAction(tr("&Import from File..."), this,
|
|
SLOT (importFile()), tr ("Ctrl+I"));
|
|
rideMenu->addAction(tr("Find &best intervals..."), this,
|
|
SLOT(findBestIntervals()), tr ("Ctrl+B"));
|
|
rideMenu->addAction(tr("Split &ride..."), this,
|
|
SLOT(splitRide()));
|
|
rideMenu->addAction(tr("D&elete ride..."), this,
|
|
SLOT(deleteRide()));
|
|
rideMenu->addAction(tr("&Manual ride entry..."), this,
|
|
SLOT(manualRide()), tr("Ctrl+M"));
|
|
|
|
QMenu *optionsMenu = menuBar()->addMenu(tr("&Tools"));
|
|
optionsMenu->addAction(tr("&Options..."), this,
|
|
SLOT(showOptions()), tr("Ctrl+O"));
|
|
optionsMenu->addAction(tr("&Tools..."), this,
|
|
SLOT(showTools()), tr("Ctrl+T"));
|
|
//optionsMenu->addAction(tr("&Reset Metrics..."), this,
|
|
// SLOT(importRideToDB()), tr("Ctrl+R"));
|
|
//optionsMenu->addAction(tr("&Update Metrics..."), this,
|
|
// SLOT(scanForMissing()()), tr("Ctrl+U"));
|
|
|
|
|
|
QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
|
|
helpMenu->addAction(tr("&About GoldenCheetah"), this, SLOT(aboutDialog()));
|
|
|
|
|
|
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked);
|
|
if(isAscending.toInt()>0){
|
|
if (last != NULL)
|
|
treeWidget->setCurrentItem(last);
|
|
} else {
|
|
// selects the first ride in the list:
|
|
if (allRides->child(0) != NULL){
|
|
treeWidget->scrollToItem(allRides->child(0), QAbstractItemView::EnsureVisible);
|
|
treeWidget->setCurrentItem(allRides->child(0));
|
|
}
|
|
}
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
}
|
|
|
|
void
|
|
MainWindow::dragEnterEvent(QDragEnterEvent *event)
|
|
{
|
|
event->acceptProposedAction(); // whatever you wanna drop we will try and process!
|
|
}
|
|
|
|
void
|
|
MainWindow::dropEvent(QDropEvent *event)
|
|
{
|
|
QList<QUrl> urls = event->mimeData()->urls();
|
|
if (urls.isEmpty()) return;
|
|
|
|
// We have something to process then
|
|
RideImportWizard *dialog = new RideImportWizard (&urls, home, this);
|
|
dialog->process(); // do it!
|
|
return;
|
|
}
|
|
|
|
void
|
|
MainWindow::addRide(QString name, bool bSelect /*=true*/)
|
|
{
|
|
QString notesFileName;
|
|
QDateTime dt;
|
|
if (!parseRideFileName(name, ¬esFileName, &dt)) {
|
|
fprintf(stderr, "bad name: %s\n", name.toAscii().constData());
|
|
assert(false);
|
|
}
|
|
RideItem *last = new RideItem(RIDE_TYPE, home.path(),
|
|
name, dt, &zones, notesFileName);
|
|
|
|
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked); // default is ascending sort
|
|
int index = 0;
|
|
while (index < allRides->childCount()) {
|
|
QTreeWidgetItem *item = allRides->child(index);
|
|
if (item->type() != RIDE_TYPE)
|
|
continue;
|
|
RideItem *other = reinterpret_cast<RideItem*>(item);
|
|
|
|
if(isAscending.toInt() > 0 ){
|
|
if (other->dateTime > dt)
|
|
break;
|
|
} else {
|
|
if (other->dateTime < dt)
|
|
break;
|
|
}
|
|
if (other->fileName == name) {
|
|
delete allRides->takeChild(index);
|
|
break;
|
|
}
|
|
++index;
|
|
}
|
|
allRides->insertChild(index, last);
|
|
calendar->addRide(last);
|
|
cpintPlot->needToScanRides = true;
|
|
if (bSelect)
|
|
{
|
|
tabWidget->setCurrentIndex(0);
|
|
treeWidget->setCurrentItem(last);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::removeCurrentRide()
|
|
{
|
|
QTreeWidgetItem *_item = treeWidget->currentItem();
|
|
if (_item->type() != RIDE_TYPE)
|
|
return;
|
|
RideItem *item = reinterpret_cast<RideItem*>(_item);
|
|
|
|
QTreeWidgetItem *itemToSelect = NULL;
|
|
for (int x=0; x<allRides->childCount(); ++x)
|
|
{
|
|
if (item==allRides->child(x))
|
|
{
|
|
if ((x+1)<allRides->childCount())
|
|
itemToSelect = allRides->child(x+1);
|
|
else if (x>0)
|
|
itemToSelect = allRides->child(x-1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
QString strOldFileName = item->fileName;
|
|
allRides->removeChild(item);
|
|
calendar->removeRide(item);
|
|
delete item;
|
|
|
|
QFile file(home.absolutePath() + "/" + strOldFileName);
|
|
// purposefully don't remove the old ext so the user wouldn't have to figure out what the old file type was
|
|
QString strNewName = strOldFileName + ".bak";
|
|
|
|
// in case there was an existing bak file, delete it
|
|
// ignore errors since it probably isn't there.
|
|
QFile::remove(home.absolutePath() + "/" + strNewName);
|
|
|
|
if (!file.rename(home.absolutePath() + "/" + strNewName))
|
|
{
|
|
QMessageBox::critical(
|
|
this, "Rename Error",
|
|
tr("Can't rename %1 to %2")
|
|
.arg(strOldFileName).arg(strNewName));
|
|
}
|
|
|
|
// added djconnel: remove old cpi file, then update bests which are associated with the file
|
|
cpintPlot->deleteCpiFile(home.absolutePath() + "/" + ride_filename_to_cpi_filename(strOldFileName));
|
|
|
|
treeWidget->setCurrentItem(itemToSelect);
|
|
rideSelected();
|
|
cpintPlot->replot();
|
|
}
|
|
|
|
void
|
|
MainWindow::newCyclist()
|
|
{
|
|
QDir newHome = home;
|
|
newHome.cdUp();
|
|
QString name = ChooseCyclistDialog::newCyclistDialog(newHome, this);
|
|
if (!name.isEmpty()) {
|
|
newHome.cd(name);
|
|
if (!newHome.exists())
|
|
assert(false);
|
|
MainWindow *main = new MainWindow(newHome);
|
|
main->show();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::openCyclist()
|
|
{
|
|
QDir newHome = home;
|
|
newHome.cdUp();
|
|
ChooseCyclistDialog d(newHome, false);
|
|
d.setModal(true);
|
|
if (d.exec() == QDialog::Accepted) {
|
|
newHome.cd(d.choice());
|
|
if (!newHome.exists())
|
|
assert(false);
|
|
MainWindow *main = new MainWindow(newHome);
|
|
main->show();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::downloadRide()
|
|
{
|
|
(new DownloadRideDialog(this, home))->show();
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::manualRide()
|
|
{
|
|
(new ManualRideDialog(this, home, useMetricUnits))->show();
|
|
}
|
|
|
|
const RideFile *
|
|
MainWindow::currentRide()
|
|
{
|
|
if ((treeWidget->selectedItems().size() != 1)
|
|
|| (treeWidget->selectedItems().first()->type() != RIDE_TYPE)) {
|
|
return NULL;
|
|
}
|
|
return ((RideItem*) treeWidget->selectedItems().first())->ride;
|
|
}
|
|
|
|
void
|
|
MainWindow::exportXML()
|
|
{
|
|
if ((treeWidget->selectedItems().size() != 1)
|
|
|| (treeWidget->selectedItems().first()->type() != RIDE_TYPE)) {
|
|
QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!"));
|
|
return;
|
|
}
|
|
|
|
RideItem *ride = (RideItem*) treeWidget->selectedItems().first();
|
|
|
|
QString fileName = QFileDialog::getSaveFileName(
|
|
this, tr("Export XML"), QDir::homePath(), tr("XML (*.xml)"));
|
|
if (fileName.length() == 0)
|
|
return;
|
|
|
|
QString err;
|
|
QFile file(fileName);
|
|
ride->ride->writeAsXml(file, err);
|
|
if (err.length() > 0) {
|
|
QMessageBox::critical(this, tr("Export XML"),
|
|
tr("Error writing %1: %2").arg(fileName).arg(err));
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::exportCSV()
|
|
{
|
|
if ((treeWidget->selectedItems().size() != 1)
|
|
|| (treeWidget->selectedItems().first()->type() != RIDE_TYPE)) {
|
|
QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!"));
|
|
return;
|
|
}
|
|
|
|
ride = (RideItem*) treeWidget->selectedItems().first();
|
|
|
|
// Ask the user if they prefer to export with English or metric units.
|
|
QStringList items;
|
|
items << tr("Metric") << tr("English");
|
|
bool ok;
|
|
QString units = QInputDialog::getItem(
|
|
this, tr("Select Units"), tr("Units:"), items, 0, false, &ok);
|
|
if(!ok)
|
|
return;
|
|
bool useMetricUnits = (units == items[0]);
|
|
|
|
QString fileName = QFileDialog::getSaveFileName(
|
|
this, tr("Export CSV"), QDir::homePath(),
|
|
tr("Comma-Separated Values (*.csv)"));
|
|
if (fileName.length() == 0)
|
|
return;
|
|
|
|
QFile file(fileName);
|
|
if (!file.open(QFile::WriteOnly | QFile::Truncate))
|
|
{
|
|
QMessageBox::critical(this, tr("Split Ride"), tr("The file %1 can't be opened for writing").arg(fileName));
|
|
return;
|
|
}
|
|
|
|
ride->ride->writeAsCsv(file, useMetricUnits);
|
|
}
|
|
|
|
void
|
|
MainWindow::importFile()
|
|
{
|
|
QVariant lastDirVar = settings->value(GC_SETTINGS_LAST_IMPORT_PATH);
|
|
QString lastDir = (lastDirVar != QVariant())
|
|
? lastDirVar.toString() : QDir::homePath();
|
|
|
|
QStringList fileNames;
|
|
if (quarqInterpreterInstalled()) {
|
|
fileNames = QFileDialog::getOpenFileNames(
|
|
this, tr("Import from File"), lastDir,
|
|
tr("All Support Formats (*.raw *.csv *.srm *.tcx *.hrm *.wko *.qla);;Raw Powertap Files (*.raw);;Comma Separated Variable (*.csv);;SRM training files (*.srm);;Garmin Training Centre (*.tcx);;Polar Precision (*.hrm);;WKO+ Files (*.wko);;Quarq ANT+ Files (*.qla);;All files (*.*)"));
|
|
} else {
|
|
fileNames = QFileDialog::getOpenFileNames(
|
|
this, tr("Import from File"), lastDir,
|
|
tr("All Support Formats (*.raw *.csv *.srm *.tcx *.hrm *.wko);;Raw Powertap Files (*.raw);;Comma Separated Variable (*.csv);;SRM training files (*.srm);;Garmin Training Centre (*.tcx);;Polar Precision (*.hrm);;WKO+ Files (*.wko);;All files (*.*)"));
|
|
|
|
}
|
|
|
|
if (!fileNames.isEmpty()) {
|
|
lastDir = QFileInfo(fileNames.front()).absolutePath();
|
|
settings->setValue(GC_SETTINGS_LAST_IMPORT_PATH, lastDir);
|
|
QStringList fileNamesCopy = fileNames; // QT doc says iterate over a copy
|
|
RideImportWizard *import = new RideImportWizard(fileNamesCopy, home, this);
|
|
import->process();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::findBestIntervals()
|
|
{
|
|
(new BestIntervalDialog(this))->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::rideSelected()
|
|
{
|
|
assert(treeWidget->selectedItems().size() <= 1);
|
|
if (treeWidget->selectedItems().isEmpty()) {
|
|
rideSummary->clear();
|
|
return;
|
|
}
|
|
|
|
QTreeWidgetItem *which = treeWidget->selectedItems().first();
|
|
if (which->type() != RIDE_TYPE) {
|
|
rideSummary->clear();
|
|
return;
|
|
}
|
|
|
|
ride = (RideItem*) which;
|
|
calendar->setSelectedDate(ride->dateTime.date());
|
|
rideSummary->setHtml(ride->htmlSummary());
|
|
rideSummary->setAlignment(Qt::AlignCenter);
|
|
if (ride) {
|
|
|
|
setAllPlotWidgets(ride);
|
|
allPlot->setData(ride);
|
|
|
|
// set the histogram data
|
|
powerHist->setData(ride);
|
|
// make sure the histogram has a legal selection
|
|
powerHist->fixSelection();
|
|
// update the options in the histogram combobox
|
|
setHistWidgets(ride);
|
|
|
|
pfPvPlot->setData(ride);
|
|
// update the QLabel widget with the CP value set in PfPvPlot::setData()
|
|
qaCPValue->setText(QString("%1").arg(pfPvPlot->getCP()));
|
|
|
|
// turn off tabs that don't make sense for manual file entry
|
|
if (ride->ride && ride->ride->deviceType() == QString("Manual CSV")) {
|
|
tabWidget->setTabEnabled(1,false); // Ride Plot
|
|
tabWidget->setTabEnabled(3,false); // Power Histogram
|
|
tabWidget->setTabEnabled(4,false); // PF/PV Plot
|
|
}
|
|
else {
|
|
// enable
|
|
tabWidget->setTabEnabled(1,true); // Ride Plot
|
|
tabWidget->setTabEnabled(3,true); // Power Histogram
|
|
tabWidget->setTabEnabled(4,true); // PF/PV Plot
|
|
}
|
|
}
|
|
if (tabWidget->currentIndex() == 2) {
|
|
cpintPlot->calculate(ride);
|
|
cpintSetCPButton->setEnabled(cpintPlot->cp > 0);
|
|
}
|
|
|
|
// generate a weekly summary of the week associated with the current ride
|
|
generateWeeklySummary();
|
|
}
|
|
|
|
|
|
void MainWindow::getBSFactors(float &timeBS, float &distanceBS)
|
|
{
|
|
|
|
int rides;
|
|
double seconds, distance, bs, convertUnit;
|
|
RideItem * lastRideItem;
|
|
QProgressDialog * progress;
|
|
bool aborted = false;
|
|
seconds = rides = 0;
|
|
distance = bs = 0;
|
|
timeBS = distanceBS = 0.0;
|
|
|
|
QVariant BSdays = settings->value(GC_BIKESCOREDAYS);
|
|
if (BSdays.isNull() || BSdays.toInt() == 0)
|
|
BSdays.setValue(30); // by default look back no more than 30 days
|
|
|
|
// if there are rides, find most recent ride so we count back from there:
|
|
if (allRides->childCount() > 0)
|
|
lastRideItem = (RideItem*) allRides->child(allRides->childCount() - 1);
|
|
else
|
|
lastRideItem = ride; // not enough rides, use current ride
|
|
|
|
// set up progress bar
|
|
progress = new QProgressDialog(QString(tr("Computing bike score estimating factors.\n")),
|
|
tr("Abort"),0,BSdays.toInt(),this);
|
|
int endingOffset = progress->labelText().size();
|
|
|
|
for (int i = 0; i < allRides->childCount(); ++i) {
|
|
RideItem *item = (RideItem*) allRides->child(i);
|
|
int days = item->dateTime.daysTo(lastRideItem->dateTime);
|
|
if (
|
|
(item->type() == RIDE_TYPE) &&
|
|
// (item->ride) &&
|
|
(days >= 0) &&
|
|
(days < BSdays.toInt())
|
|
) {
|
|
|
|
RideMetric *m;
|
|
item->htmlSummary(); // compute metrics
|
|
|
|
QString existing = progress->labelText();
|
|
existing.chop(progress->labelText().size() - endingOffset);
|
|
progress->setLabelText(
|
|
existing + QString(tr("Processing %1...")).arg(item->fileName));
|
|
|
|
|
|
// only count rides with BS > 0
|
|
if ((m = item->metrics.value("skiba_bike_score")) &&
|
|
m->value(true)) {
|
|
bs += m->value(true);
|
|
|
|
if ((m = item->metrics.value("time_riding"))) {
|
|
seconds += m->value(true);
|
|
}
|
|
|
|
if ((m = item->metrics.value("total_distance"))) {
|
|
distance += m->value(true);
|
|
}
|
|
|
|
rides++;
|
|
}
|
|
// check progress
|
|
QCoreApplication::processEvents();
|
|
if (progress->wasCanceled()) {
|
|
aborted = true;
|
|
goto done;
|
|
}
|
|
// set progress from 0 to BSdays
|
|
progress->setValue(BSdays.toInt() - days);
|
|
|
|
}
|
|
}
|
|
if (rides) {
|
|
// convert distance from metric:
|
|
if (!useMetricUnits)
|
|
{
|
|
const double MILES_PER_KM = 0.62137119;
|
|
convertUnit = MILES_PER_KM;
|
|
}
|
|
else {
|
|
convertUnit = 1.0;
|
|
}
|
|
distance *= convertUnit;
|
|
|
|
timeBS = (bs * 3600) / seconds; // BS per hour
|
|
distanceBS = bs / distance; // BS per mile or km
|
|
}
|
|
done:
|
|
if (aborted) {
|
|
timeBS = distanceBS = 0;
|
|
}
|
|
|
|
delete progress;
|
|
}
|
|
|
|
void MainWindow::generateWeeklySummary()
|
|
{
|
|
QDate wstart = ride->dateTime.date();
|
|
wstart = wstart.addDays(Qt::Monday - wstart.dayOfWeek());
|
|
assert(wstart.dayOfWeek() == Qt::Monday);
|
|
QDate wend = wstart.addDays(7);
|
|
const RideMetricFactory &factory = RideMetricFactory::instance();
|
|
QSharedPointer<RideMetric> weeklySeconds(factory.newMetric("time_riding"));
|
|
assert(weeklySeconds);
|
|
QSharedPointer<RideMetric> weeklyDistance(factory.newMetric("total_distance"));
|
|
assert(weeklyDistance);
|
|
QSharedPointer<RideMetric> weeklyWork(factory.newMetric("total_work"));
|
|
assert(weeklyWork);
|
|
QSharedPointer<RideMetric> weeklyBS(factory.newMetric("skiba_bike_score"));
|
|
assert(weeklyBS);
|
|
QSharedPointer<RideMetric> weeklyRelIntensity(factory.newMetric("skiba_relative_intensity"));
|
|
assert(weeklyRelIntensity);
|
|
|
|
QSharedPointer<RideMetric> dailySeconds[7];
|
|
QSharedPointer<RideMetric> dailyDistance[7];
|
|
QSharedPointer<RideMetric> dailyBS[7];
|
|
QSharedPointer<RideMetric> dailyRI[7];
|
|
QSharedPointer<RideMetric> dailyW[7];
|
|
QSharedPointer<RideMetric> dailyXP[7];
|
|
|
|
for (int i = 0; i < 7; i++) {
|
|
dailySeconds[i] = QSharedPointer<RideMetric>(factory.newMetric("time_riding"));
|
|
assert(dailySeconds[i]);
|
|
dailyDistance[i] = QSharedPointer<RideMetric>(factory.newMetric("total_distance"));
|
|
assert(dailyDistance[i]);
|
|
dailyBS[i] = QSharedPointer<RideMetric>(factory.newMetric("skiba_bike_score"));
|
|
assert(dailyBS[i]);
|
|
dailyRI[i] = QSharedPointer<RideMetric>(factory.newMetric("skiba_relative_intensity"));
|
|
assert(dailyRI[i]);
|
|
dailyW[i] = QSharedPointer<RideMetric>(factory.newMetric("total_work"));
|
|
assert(dailyW[i]);
|
|
dailyXP[i] = QSharedPointer<RideMetric>(factory.newMetric("skiba_xpower"));
|
|
assert(dailyXP[i]);
|
|
}
|
|
|
|
int zone_range = -1;
|
|
QVector<double> time_in_zone;
|
|
int num_zones = -1;
|
|
bool zones_ok = true;
|
|
|
|
for (int i = 0; i < allRides->childCount(); ++i) {
|
|
RideItem *item = (RideItem*) allRides->child(i);
|
|
int day;
|
|
if (
|
|
(item->type() == RIDE_TYPE) &&
|
|
((day = wstart.daysTo(item->dateTime.date())) >= 0) &&
|
|
(day < 7)
|
|
) {
|
|
|
|
item->htmlSummary(); // generates item->ride
|
|
if (!item->ride)
|
|
continue;
|
|
|
|
RideMetric *m;
|
|
item->htmlSummary(); // compute metrics
|
|
if ((m = item->metrics.value(weeklySeconds->name()))) {
|
|
weeklySeconds->aggregateWith(m);
|
|
dailySeconds[day]->aggregateWith(m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyDistance->name()))) {
|
|
weeklyDistance->aggregateWith(m);
|
|
dailyDistance[day]->aggregateWith(m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyWork->name()))) {
|
|
weeklyWork->aggregateWith(m);
|
|
dailyW[day]->aggregateWith(m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyBS->name()))) {
|
|
weeklyBS->aggregateWith(m);
|
|
dailyBS[day]->aggregateWith(m);
|
|
}
|
|
|
|
if ((m = item->metrics.value(weeklyRelIntensity->name()))) {
|
|
weeklyRelIntensity->aggregateWith(m);
|
|
dailyRI[day]->aggregateWith(m);
|
|
}
|
|
|
|
if ((m = item->metrics.value("skiba_xpower")))
|
|
dailyXP[day]->aggregateWith(m);
|
|
|
|
// compute time in zones
|
|
if (zones) {
|
|
if (zone_range == -1) {
|
|
zone_range = item->zoneRange();
|
|
num_zones = item->numZones();
|
|
time_in_zone.clear();
|
|
time_in_zone.resize(num_zones);
|
|
}
|
|
else if (item->zoneRange() != zone_range) {
|
|
zones_ok = false;
|
|
}
|
|
if (zone_range != -1) {
|
|
for (int j = 0; j < num_zones; ++j)
|
|
time_in_zone[j] += item->timeInZone(j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int seconds = ((int) round(weeklySeconds->value(true)));
|
|
int minutes = seconds / 60;
|
|
seconds %= 60;
|
|
int hours = minutes / 60;
|
|
minutes %= 60;
|
|
|
|
const char *dateFormat = "MM/dd/yyyy";
|
|
|
|
QString summary;
|
|
summary =
|
|
tr(
|
|
"<center>"
|
|
"<h2>Week of %1 through %2</h2>"
|
|
"<h2>Summary</h2>"
|
|
"<p>"
|
|
"<table align=\"center\" width=\"60%\" border=0>"
|
|
"<tr><td>Total time riding:</td>"
|
|
" <td align=\"right\">%3:%4:%5</td></tr>"
|
|
"<tr><td>Total distance (%6):</td>"
|
|
" <td align=\"right\">%7</td></tr>"
|
|
"<tr><td>Total work (kJ):</td>"
|
|
" <td align=\"right\">%8</td></tr>"
|
|
"<tr><td>Daily Average work (kJ):</td>"
|
|
" <td align=\"right\">%9</td></tr>"
|
|
)
|
|
.arg(wstart.toString(dateFormat))
|
|
.arg(wstart.addDays(6).toString(dateFormat))
|
|
.arg(hours)
|
|
.arg(minutes, 2, 10, QLatin1Char('0'))
|
|
.arg(seconds, 2, 10, QLatin1Char('0'))
|
|
.arg(useMetricUnits ? "km" : "miles")
|
|
.arg(weeklyDistance->value(useMetricUnits), 0, 'f', 1)
|
|
.arg((unsigned) round(weeklyWork->value(useMetricUnits)))
|
|
.arg((unsigned) round(weeklyWork->value(useMetricUnits) / 7));
|
|
|
|
double weeklyBSValue = weeklyBS->value(useMetricUnits);
|
|
bool useBikeScore = (zone_range != -1) && (weeklyBSValue > 0);
|
|
|
|
if (zone_range != -1) {
|
|
if (useBikeScore)
|
|
summary +=
|
|
tr(
|
|
"<tr><td>Total BikeScore:</td>"
|
|
" <td align=\"right\">%1</td></tr>"
|
|
"<tr><td>Net Relative Intensity:</td>"
|
|
" <td align=\"right\">%2</td></tr>"
|
|
)
|
|
.arg((unsigned) round(weeklyBSValue))
|
|
.arg(weeklyRelIntensity->value(useMetricUnits), 0, 'f', 3);
|
|
|
|
summary +=
|
|
tr(
|
|
"</table>"
|
|
"<h2>Power Zones</h2>"
|
|
);
|
|
if (!zones_ok)
|
|
summary += "Error: Week spans more than one zone range.";
|
|
else {
|
|
summary += zones->summarize(zone_range, time_in_zone);
|
|
}
|
|
}
|
|
|
|
summary += "</center>";
|
|
|
|
// set the axis labels of the weekly plots
|
|
QwtText textLabel = QwtText(useMetricUnits ? "km" : "miles");
|
|
QFont weeklyPlotAxisTitleFont = textLabel.font();
|
|
weeklyPlotAxisTitleFont.setPointSize(10);
|
|
weeklyPlotAxisTitleFont.setBold(true);
|
|
textLabel.setFont(weeklyPlotAxisTitleFont);
|
|
weeklyPlot->setAxisTitle(QwtPlot::yLeft, textLabel);
|
|
textLabel.setText("Minutes");
|
|
weeklyPlot->setAxisTitle(QwtPlot::yRight, textLabel);
|
|
textLabel.setText(useBikeScore ? "BikeScore" : "kJoules");
|
|
weeklyBSPlot->setAxisTitle(QwtPlot::yLeft, textLabel);
|
|
textLabel.setText(useBikeScore ? "Intensity" : "xPower");
|
|
weeklyBSPlot->setAxisTitle(QwtPlot::yRight, textLabel);
|
|
|
|
// for the daily distance/duration and bikescore plots:
|
|
// first point: establish zero position
|
|
// points 2N, 2N+1: Nth day (N from 1 to 7), up then down
|
|
// 16th point: move to draw baseline off right of plot
|
|
|
|
double xdist[16];
|
|
double xdur[16];
|
|
double xbsorw[16];
|
|
double xriorxp[16];
|
|
|
|
double ydist[16]; // daily distance
|
|
double ydur[16]; // daily total time
|
|
double ybsorw[16]; // daily minutes
|
|
double yriorxp[16]; // daily relative intensity
|
|
|
|
// data for a "baseline" curve to draw a baseline
|
|
double xbaseline[] = {0, 8};
|
|
double ybaseline[] = {0, 0};
|
|
weeklyBaselineCurve->setData(xbaseline, ybaseline, 2);
|
|
weeklyBSBaselineCurve->setData(xbaseline, ybaseline, 2);
|
|
|
|
const double bar_width = 0.3;
|
|
|
|
int i = 0;
|
|
xdist[i] =
|
|
xdur[i] =
|
|
xbsorw[i] =
|
|
xriorxp[i] =
|
|
0;
|
|
|
|
ydist[i] =
|
|
ydur[i] =
|
|
ybsorw[i] =
|
|
yriorxp[i] =
|
|
0;
|
|
|
|
for(int day = 0; day < 7; day++){
|
|
double x;
|
|
|
|
i++;
|
|
xdist[i] = x = day + 1 - bar_width;
|
|
xdist[i + 1] = x += bar_width;
|
|
xdur[i] = x;
|
|
xdur[i + 1] = x += bar_width;
|
|
|
|
xbsorw[i] = x = day + 1 - bar_width;
|
|
xbsorw[i + 1] = x += bar_width;
|
|
xriorxp[i] = x;
|
|
xriorxp[i + 1] = x += bar_width;
|
|
|
|
ydist[i] = dailyDistance[day]->value(useMetricUnits);
|
|
ydur[i] = dailySeconds[day]->value(useMetricUnits) / 60;
|
|
ybsorw[i] = useBikeScore ? dailyBS[day]->value(useMetricUnits) : dailyW[day]->value(useMetricUnits) / 1000;
|
|
yriorxp[i] = useBikeScore ? dailyRI[day]->value(useMetricUnits) : dailyXP[day]->value(useMetricUnits);
|
|
|
|
i++;
|
|
ydist[i] = 0;
|
|
ydur[i] = 0;
|
|
ybsorw[i] = 0;
|
|
yriorxp[i] = 0;
|
|
}
|
|
|
|
// sweep a baseline off the right of the plot
|
|
i++;
|
|
xdist[i] =
|
|
xdur[i] =
|
|
xbsorw[i] =
|
|
xriorxp[i] =
|
|
8;
|
|
|
|
ydist[i] =
|
|
ydur[i] =
|
|
ybsorw[i] =
|
|
yriorxp[i] =
|
|
0;
|
|
|
|
// Distance/Duration plot:
|
|
weeklyDistCurve->setData(xdist, ydist, 16);
|
|
weeklyPlot->setAxisScale(QwtPlot::yLeft, 0, weeklyDistCurve->maxYValue()*1.1, 0);
|
|
weeklyPlot->setAxisScale(QwtPlot::xBottom, 0.5, 7.5, 0);
|
|
weeklyPlot->setAxisTitle(QwtPlot::yLeft, useMetricUnits ? "Kilometers" : "Miles");
|
|
|
|
weeklyDurationCurve->setData(xdur, ydur, 16);
|
|
weeklyPlot->setAxisScale(QwtPlot::yRight, 0, weeklyDurationCurve->maxYValue()*1.1, 0);
|
|
weeklyPlot->replot();
|
|
|
|
// BikeScore/Relative Intensity plot
|
|
weeklyBSCurve->setData(xbsorw, ybsorw, 16);
|
|
weeklyBSPlot->setAxisScale(QwtPlot::yLeft, 0, weeklyBSCurve->maxYValue()*1.1, 0);
|
|
weeklyBSPlot->setAxisScale(QwtPlot::xBottom, 0.5, 7.5, 0);
|
|
|
|
// set axis minimum for relative intensity
|
|
double RImin = -1;
|
|
for(int i = 1; i < 16; i += 2)
|
|
if (yriorxp[i] > 0 && ((RImin < 0) || (yriorxp[i] < RImin)))
|
|
RImin = yriorxp[i];
|
|
if (RImin < 0)
|
|
RImin = 0;
|
|
RImin *= 0.8;
|
|
for (int i = 0; i < 16; i ++)
|
|
if (yriorxp[i] < RImin)
|
|
yriorxp[i] = RImin;
|
|
weeklyRICurve->setBaseline(RImin);
|
|
weeklyRICurve->setData(xriorxp, yriorxp, 16);
|
|
weeklyBSPlot->setAxisScale(QwtPlot::yRight, RImin, weeklyRICurve->maxYValue()*1.1, 0);
|
|
|
|
weeklyBSPlot->replot();
|
|
|
|
|
|
weeklySummary->setHtml(summary);
|
|
|
|
// First save the contents of the notes window.
|
|
saveNotes();
|
|
|
|
// Now open any notes associated with the new ride.
|
|
rideNotes->setPlainText("");
|
|
QString notesPath = home.absolutePath() + "/" + ride->notesFileName;
|
|
QFile notesFile(notesPath);
|
|
|
|
if (notesFile.exists()) {
|
|
if (notesFile.open(QFile::ReadOnly | QFile::Text)) {
|
|
QTextStream in(¬esFile);
|
|
rideNotes->setPlainText(in.readAll());
|
|
notesFile.close();
|
|
}
|
|
else {
|
|
QMessageBox::critical(
|
|
this, tr("Read Error"),
|
|
tr("Can't read notes file %1").arg(notesPath));
|
|
}
|
|
}
|
|
|
|
currentNotesFile = ride->notesFileName;
|
|
currentNotesChanged = false;
|
|
}
|
|
|
|
void MainWindow::saveNotes()
|
|
{
|
|
if ((currentNotesFile != "") && currentNotesChanged) {
|
|
QString notesPath =
|
|
home.absolutePath() + "/" + currentNotesFile;
|
|
QString tmpPath = notesPath + ".tmp";
|
|
QFile tmp(tmpPath);
|
|
if (tmp.open(QFile::WriteOnly | QFile::Truncate)) {
|
|
QTextStream out(&tmp);
|
|
out << rideNotes->toPlainText();
|
|
tmp.close();
|
|
QFile::remove(notesPath);
|
|
if (rename(tmpPath.toAscii().constData(),
|
|
notesPath.toAscii().constData()) == -1) {
|
|
QMessageBox::critical(
|
|
this, tr("Write Error"),
|
|
tr("Can't rename %1 to %2")
|
|
.arg(tmpPath).arg(notesPath));
|
|
}
|
|
}
|
|
else {
|
|
QMessageBox::critical(
|
|
this, tr("Write Error"),
|
|
tr("Can't write notes file %1").arg(tmpPath));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::resizeEvent(QResizeEvent*)
|
|
{
|
|
settings->setValue(GC_SETTINGS_MAIN_GEOM, geometry());
|
|
}
|
|
|
|
void
|
|
MainWindow::showOptions()
|
|
{
|
|
ConfigDialog *cd = new ConfigDialog(home, &zones);
|
|
cd->exec();
|
|
|
|
// update other items in case zones were changed
|
|
if (ride) {
|
|
// daily summary
|
|
rideSummary->setHtml(ride->htmlSummary());
|
|
|
|
// weekly summary
|
|
generateWeeklySummary();
|
|
|
|
// all plot
|
|
allPlot->refreshZoneLabels();
|
|
allPlot->replot();
|
|
|
|
// histogram
|
|
powerHist->refreshZoneLabels();
|
|
powerHist->replot();
|
|
|
|
// force-versus-pedal velocity plot
|
|
pfPvPlot->refreshZoneItems();
|
|
pfPvPlot->replot();
|
|
qaCPValue->setText(QString("%1").arg(pfPvPlot->getCP()));
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::moveEvent(QMoveEvent*)
|
|
{
|
|
settings->setValue(GC_SETTINGS_MAIN_GEOM, geometry());
|
|
}
|
|
|
|
void
|
|
MainWindow::closeEvent(QCloseEvent*)
|
|
{
|
|
saveNotes();
|
|
}
|
|
|
|
void
|
|
MainWindow::leftLayoutMoved()
|
|
{
|
|
settings->setValue(GC_SETTINGS_CALENDAR_SIZES, leftLayout->saveState());
|
|
}
|
|
|
|
void
|
|
MainWindow::splitterMoved()
|
|
{
|
|
settings->setValue(GC_SETTINGS_SPLITTER_SIZES, splitter->saveState());
|
|
}
|
|
|
|
void
|
|
MainWindow::setSmoothingFromSlider()
|
|
{
|
|
if (allPlot->smoothing() != smoothSlider->value()) {
|
|
allPlot->setSmoothing(smoothSlider->value());
|
|
smoothLineEdit->setText(QString("%1").arg(allPlot->smoothing()));
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setSmoothingFromLineEdit()
|
|
{
|
|
int value = smoothLineEdit->text().toInt();
|
|
if (value != allPlot->smoothing()) {
|
|
allPlot->setSmoothing(value);
|
|
smoothSlider->setValue(value);
|
|
}
|
|
}
|
|
|
|
// set the rider value of CP to the value derived from the CP model extraction
|
|
void
|
|
MainWindow::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;
|
|
}
|
|
|
|
if (zones == NULL)
|
|
// set up new zones
|
|
zones = new Zones();
|
|
|
|
|
|
// determine in which range to write the value: use the range associated with the presently selected ride
|
|
int range;
|
|
if (ride)
|
|
range = ride->zoneRange();
|
|
else {
|
|
QDate today = QDate::currentDate();
|
|
range = zones->whichRange(today);
|
|
}
|
|
|
|
// add a new range if we failed to find a valid one
|
|
if (range < 0) {
|
|
// create an infinite range
|
|
zones->addZoneRange(cp);
|
|
range = 0;
|
|
}
|
|
|
|
zones->setCP(range, cp); // update the CP value
|
|
zones->setZonesFromCP(range); // update the zones based on the value of CP
|
|
zones->write(home); // write the output file
|
|
|
|
QDate startDate = zones->getStartDate(range);
|
|
QDate endDate = zones->getEndDate(range);
|
|
QMessageBox::information(
|
|
this,
|
|
tr("CP saved"),
|
|
tr("Range from %1 to %2\nRider CP set to %3 watts") .
|
|
arg(startDate.isNull() ? "BEGIN" : startDate.toString()) .
|
|
arg(endDate.isNull() ? "END" : endDate.toString()) .
|
|
arg(cp)
|
|
);
|
|
|
|
// regenerate the ride and weekly summaries associated with the present ride
|
|
if (ride) {
|
|
// daily summary
|
|
rideSummary->setHtml(ride->htmlSummary());
|
|
|
|
// weekly summary
|
|
generateWeeklySummary();
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
MainWindow::setBinWidthFromSlider()
|
|
{
|
|
if (powerHist->binWidth() != binWidthSlider->value()) {
|
|
powerHist->setBinWidth(binWidthSlider->value());
|
|
setHistBinWidthText();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setlnYHistFromCheckBox()
|
|
{
|
|
if (powerHist->islnY() != lnYHistCheckBox->isChecked())
|
|
powerHist->setlnY(! powerHist->islnY());
|
|
}
|
|
|
|
void
|
|
MainWindow::setWithZerosFromCheckBox()
|
|
{
|
|
if (powerHist->withZeros() != withZerosCheckBox->isChecked()) {
|
|
powerHist->setWithZeros(withZerosCheckBox->isChecked());
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setHistBinWidthText()
|
|
{
|
|
binWidthLineEdit->setText(QString("%1").arg(powerHist->getBinWidthRealUnits(), 0, 'g', 3));
|
|
}
|
|
|
|
void
|
|
MainWindow::setHistTextValidator()
|
|
{
|
|
double delta = powerHist->getDelta();
|
|
int digits = powerHist->getDigits();
|
|
binWidthLineEdit->setValidator(
|
|
(digits == 0) ?
|
|
(QValidator *) (
|
|
new QIntValidator(binWidthSlider->minimum() * delta,
|
|
binWidthSlider->maximum() * delta,
|
|
binWidthLineEdit
|
|
)
|
|
) :
|
|
(QValidator *) (
|
|
new QDoubleValidator(binWidthSlider->minimum() * delta,
|
|
binWidthSlider->maximum() * delta,
|
|
digits,
|
|
binWidthLineEdit
|
|
)
|
|
)
|
|
);
|
|
|
|
}
|
|
|
|
void
|
|
MainWindow::setHistSelection(int id)
|
|
{
|
|
if (id == histWattsShadedID)
|
|
powerHist->setSelection(PowerHist::wattsShaded);
|
|
else if (id == histWattsUnshadedID)
|
|
powerHist->setSelection(PowerHist::wattsUnshaded);
|
|
else if (id == histNmID)
|
|
powerHist->setSelection(PowerHist::nm);
|
|
else if (id == histHrID)
|
|
powerHist->setSelection(PowerHist::hr);
|
|
else if (id == histKphID)
|
|
powerHist->setSelection(PowerHist::kph);
|
|
else if (id == histCadID)
|
|
powerHist->setSelection(PowerHist::cad);
|
|
else
|
|
fprintf(stderr, "Illegal id encountered: %d", id);
|
|
|
|
setHistBinWidthText();
|
|
setHistTextValidator();
|
|
}
|
|
|
|
void
|
|
MainWindow::setShadeZonesPfPvFromCheckBox()
|
|
{
|
|
if (pfPvPlot->shadeZones() != shadeZonesPfPvCheckBox->isChecked()) {
|
|
pfPvPlot->setShadeZones(shadeZonesPfPvCheckBox->isChecked());
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setBinWidthFromLineEdit()
|
|
{
|
|
double value = binWidthLineEdit->text().toDouble();
|
|
if (value != powerHist->binWidth()) {
|
|
binWidthSlider->setValue(powerHist->setBinWidthRealUnits(value));
|
|
setHistBinWidthText();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setQaCPFromLineEdit()
|
|
{
|
|
int value = qaCPValue->text().toInt();
|
|
pfPvPlot->setCP(value);
|
|
}
|
|
|
|
void
|
|
MainWindow::setQaCADFromLineEdit()
|
|
{
|
|
int value = qaCadValue->text().toInt();
|
|
pfPvPlot->setCAD(value);
|
|
}
|
|
|
|
void
|
|
MainWindow::setQaCLFromLineEdit()
|
|
{
|
|
double value = qaClValue->text().toDouble();
|
|
pfPvPlot->setCL(value);
|
|
}
|
|
|
|
void
|
|
MainWindow::tabChanged(int index)
|
|
{
|
|
if (index == 2) {
|
|
if (treeWidget->selectedItems().size() == 1) {
|
|
QTreeWidgetItem *which = treeWidget->selectedItems().first();
|
|
if (which->type() == RIDE_TYPE) {
|
|
RideItem *ride = (RideItem*) which;
|
|
cpintPlot->calculate(ride);
|
|
cpintSetCPButton->setEnabled(cpintPlot->cp > 0);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static unsigned
|
|
curve_to_point(double x, const QwtPlotCurve *curve)
|
|
{
|
|
unsigned result = 0;
|
|
if (curve) {
|
|
const QwtData &data = curve->data();
|
|
if (data.size() > 0) {
|
|
unsigned min = 0, mid = 0, max = data.size();
|
|
while (min < max - 1) {
|
|
mid = (max - min) / 2 + min;
|
|
if (x < data.x(mid)) {
|
|
result = (unsigned) round(data.y(mid));
|
|
max = mid;
|
|
}
|
|
else {
|
|
min = mid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void
|
|
MainWindow::pickerMoved(const QPoint &pos)
|
|
{
|
|
double minutes = cpintPlot->invTransform(QwtPlot::xBottom, pos.x());
|
|
cpintTimeValue->setText(interval_to_str(60.0*minutes));
|
|
|
|
// current ride
|
|
{
|
|
unsigned watts = curve_to_point(minutes, cpintPlot->getThisCurve());
|
|
QString label;
|
|
if (watts > 0)
|
|
label = QString("%1 watts").arg(watts);
|
|
else
|
|
label = tr("no data");
|
|
cpintTodayValue->setText(label);
|
|
}
|
|
|
|
// global ride
|
|
{
|
|
QString label;
|
|
int index = (int) ceil(minutes * 60);
|
|
if (cpintPlot->getBests().count() > index) {
|
|
QDate date = cpintPlot->getBestDates()[index];
|
|
label =
|
|
QString("%1 watts (%2)").
|
|
arg(cpintPlot->getBests()[index]).
|
|
arg(date.isValid() ? date.toString("MM/dd/yyyy") : "no date");
|
|
}
|
|
else
|
|
label = tr("no data");
|
|
cpintAllValue->setText(label);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::aboutDialog()
|
|
{
|
|
QMessageBox::about(this, tr("About GoldenCheetah"), tr(
|
|
"<center>"
|
|
"<h2>GoldenCheetah</h2>"
|
|
"<i>Cycling Power Analysis Software for Linux, Mac, and Windows</i>"
|
|
"<p><i>Build date: "
|
|
"") + QString(__DATE__) + " " + QString(__TIME__) + "</i>"
|
|
"<p><i>Version: " + QString(GC_VERSION) + ("</i>"
|
|
"<p>GoldenCheetah is licensed under the "
|
|
"<a href=\"http://www.gnu.org/copyleft/gpl.html\">GNU General "
|
|
"Public License</a>."
|
|
"<p>Source code can be obtained from "
|
|
"<a href=\"http://goldencheetah.org/\">"
|
|
"http://goldencheetah.org/</a>."
|
|
"</center>"
|
|
));
|
|
}
|
|
|
|
|
|
void MainWindow::importRideToDB()
|
|
{
|
|
MetricAggregator aggregator;
|
|
aggregator.aggregateRides(home, zones);
|
|
}
|
|
|
|
void MainWindow::scanForMissing()
|
|
{
|
|
MetricAggregator aggregator;
|
|
aggregator.scanForMissing(home, zones);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
MainWindow::notesChanged()
|
|
{
|
|
currentNotesChanged = true;
|
|
}
|
|
|
|
void MainWindow::showTools()
|
|
{
|
|
ToolsDialog *td = new ToolsDialog();
|
|
td->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::splitRide()
|
|
{
|
|
(new SplitRideDialog(this))->exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::deleteRide()
|
|
{
|
|
QTreeWidgetItem *_item = treeWidget->currentItem();
|
|
if (_item==NULL || _item->type() != RIDE_TYPE)
|
|
return;
|
|
RideItem *item = reinterpret_cast<RideItem*>(_item);
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("Are you sure you want to delete the ride:"));
|
|
msgBox.setInformativeText(item->fileName);
|
|
QPushButton *deleteButton = msgBox.addButton(tr("Delete"),QMessageBox::YesRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(QMessageBox::Cancel);
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.exec();
|
|
if(msgBox.clickedButton() == deleteButton)
|
|
removeCurrentRide();
|
|
}
|
|
|
|
void MainWindow::setAllPlotWidgets(RideItem *ride)
|
|
{
|
|
if (ride->ride) {
|
|
RideFileDataPresent *dataPresent = ride->ride->areDataPresent();
|
|
showPower->setEnabled(dataPresent->watts);
|
|
showHr->setEnabled(dataPresent->hr);
|
|
showSpeed->setEnabled(dataPresent->kph);
|
|
showCad->setEnabled(dataPresent->cad);
|
|
showAlt->setEnabled(dataPresent->alt);
|
|
allPlot->showPower(showPower->currentIndex());
|
|
allPlot->wattsCurve->setVisible(dataPresent->watts && (showPower->currentIndex() < 2));
|
|
allPlot->hrCurve->setVisible(dataPresent->hr && showHr->isChecked());
|
|
allPlot->speedCurve->setVisible(dataPresent->kph && showSpeed->isChecked());
|
|
allPlot->cadCurve->setVisible(dataPresent->cad && showCad->isChecked());
|
|
allPlot->altCurve->setVisible(dataPresent->alt && showAlt->isChecked());
|
|
}
|
|
else {
|
|
showPower->setEnabled(false);
|
|
showHr->setEnabled(false);
|
|
showSpeed->setEnabled(false);
|
|
showCad->setEnabled(false);
|
|
showAlt->setEnabled(false);
|
|
allPlot->showPower(false);
|
|
allPlot->wattsCurve->setVisible(false);
|
|
allPlot->hrCurve->setVisible(false);
|
|
allPlot->speedCurve->setVisible(false);
|
|
allPlot->cadCurve->setVisible(false);
|
|
}
|
|
}
|
|
|
|
void MainWindow::setHistWidgets(RideItem *rideItem)
|
|
{
|
|
int count = 0;
|
|
assert(rideItem);
|
|
RideFile *ride = rideItem->ride;
|
|
|
|
// prevent selection from changing during reconstruction of options
|
|
disconnect(histParameterCombo, SIGNAL(currentIndexChanged(int)),
|
|
this, SLOT(setHistSelection(int)));
|
|
|
|
if (ride) {
|
|
// we want to retain the present selection
|
|
PowerHist::Selection s = powerHist->selection();
|
|
|
|
histParameterCombo->clear();
|
|
|
|
|
|
histWattsShadedID =
|
|
histWattsUnshadedID =
|
|
histNmID =
|
|
histHrID =
|
|
histKphID =
|
|
histCadID =
|
|
-1;
|
|
|
|
if (ride->areDataPresent()->watts) {
|
|
histWattsShadedID = count ++;
|
|
histParameterCombo->addItem(tr("Watts(shaded)"));
|
|
histWattsUnshadedID = count ++;
|
|
histParameterCombo->addItem(tr("Watts(unshaded)"));
|
|
}
|
|
if (ride->areDataPresent()->nm) {
|
|
histNmID = count ++;
|
|
histParameterCombo->addItem(tr("Torque"));
|
|
}
|
|
if (ride->areDataPresent()->hr) {
|
|
histHrID = count ++;
|
|
histParameterCombo->addItem(tr("Heartrate"));
|
|
}
|
|
if (ride->areDataPresent()->kph) {
|
|
histKphID = count ++;
|
|
histParameterCombo->addItem(tr("Speed"));
|
|
}
|
|
if (ride->areDataPresent()->cad) {
|
|
histCadID = count ++;
|
|
histParameterCombo->addItem(tr("Cadence"));
|
|
}
|
|
if (count > 0) {
|
|
histParameterCombo->setEnabled(true);
|
|
binWidthLineEdit->setEnabled(true);
|
|
binWidthSlider->setEnabled(true);
|
|
withZerosCheckBox->setEnabled(true);
|
|
lnYHistCheckBox->setEnabled(true);
|
|
|
|
// set widget to proper value
|
|
if ((s == PowerHist::wattsShaded) && (histWattsShadedID >= 0))
|
|
histParameterCombo->setCurrentIndex(histWattsShadedID);
|
|
else if ((s == PowerHist::wattsUnshaded) && (histWattsUnshadedID >= 0))
|
|
histParameterCombo->setCurrentIndex(histWattsUnshadedID);
|
|
else if ((s == PowerHist::nm) && (histNmID >= 0))
|
|
histParameterCombo->setCurrentIndex(histNmID);
|
|
else if ((s == PowerHist::hr) && (histHrID >= 0))
|
|
histParameterCombo->setCurrentIndex(histHrID);
|
|
else if ((s == PowerHist::kph) && (histKphID >= 0))
|
|
histParameterCombo->setCurrentIndex(histKphID);
|
|
else if ((s == PowerHist::cad) && (histCadID >= 0))
|
|
histParameterCombo->setCurrentIndex(histCadID);
|
|
else
|
|
histParameterCombo->setCurrentIndex(0);
|
|
|
|
// reconnect widget
|
|
connect(histParameterCombo, SIGNAL(currentIndexChanged(int)),
|
|
this, SLOT(setHistSelection(int)));
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
histParameterCombo->addItem(tr("no data"));
|
|
histParameterCombo->setEnabled(false);
|
|
binWidthLineEdit->setEnabled(false);
|
|
binWidthSlider->setEnabled(false);
|
|
withZerosCheckBox->setEnabled(false);
|
|
lnYHistCheckBox->setEnabled(false);
|
|
}
|
|
|
|
/*
|
|
* This slot gets called when the user picks a new date, using the mouse,
|
|
* in the calendar. We have to adjust TreeView to match.
|
|
*/
|
|
void MainWindow::dateChanged(const QDate &date)
|
|
{
|
|
for (int i = 0; i < allRides->childCount(); i++)
|
|
{
|
|
ride = (RideItem*) allRides->child(i);
|
|
if (ride->dateTime.date() == date) {
|
|
treeWidget->scrollToItem(allRides->child(i),
|
|
QAbstractItemView::EnsureVisible);
|
|
treeWidget->setCurrentItem(allRides->child(i));
|
|
i = allRides->childCount();
|
|
}
|
|
}
|
|
}
|
|
|
|
|