Ride editor and tools
A new tab 'Editor' for manually editing ride file data points and associated menu options under 'Tools' for fixing spikes, gaps, GPS errors and adjusting torque values. A revert to saved ride option is also included to 'undo' all changes. The ride editor supports undo/redo as well as cut and paste and "paste special" (to append points or swap columns/overwrite selected data series). The editor also supports search and will automatically highlight anomalous data. When a file is saved, the changes are recorded in a new metadata special field called "Change History" which can be added as a Textbox in the metadata config. The data processors can be run manually or automatically when a ride is opened - these are configured on the ride data tab in the config pane. Significant changes have been introduced in the codebase, the most significant of which are; a RideFileCommand class for modifying ride data has been introduced (as a member of RideFile) and the RideItem class is now a QObject as well as QTreeWidgetItem to enable signalling. The Ride Editor uses a RideFileTableModel that can be re-used in other parts of the code. LTMoutliers class has been introduced in support of anomaly detection in the editor (which highlights anomalies with a wiggly red line). Fixes #103.
@@ -453,8 +453,6 @@ AllPlotWindow::setEndSelection(AllPlot* plot, int xPosition, bool newInterval, Q
|
||||
//allMarker3->setZ(-1000.0);
|
||||
allMarker3->show();
|
||||
|
||||
RideFile tmpRide = RideFile();
|
||||
|
||||
QTreeWidgetItem *which = mainWindow->rideItem();
|
||||
RideItem *ride = (RideItem*)which;
|
||||
|
||||
|
||||
118
src/DataProcessor.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2010 mark Liversedge )liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DataProcessor.h"
|
||||
#include "MainWindow.h"
|
||||
#include "AllPlot.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <assert.h>
|
||||
|
||||
DataProcessorFactory *DataProcessorFactory::instance_;
|
||||
|
||||
DataProcessorFactory &DataProcessorFactory::instance()
|
||||
{
|
||||
if (!instance_) instance_ = new DataProcessorFactory();
|
||||
return *instance_;
|
||||
}
|
||||
|
||||
bool
|
||||
DataProcessorFactory::registerProcessor(QString name, DataProcessor *processor)
|
||||
{
|
||||
assert(!processors.contains(name));
|
||||
processors.insert(name, processor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
DataProcessorFactory::autoProcess(RideFile *ride)
|
||||
{
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
bool changed = false;
|
||||
|
||||
// run through the processors and execute them!
|
||||
QMapIterator<QString, DataProcessor*> i(processors);
|
||||
i.toFront();
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
QString configsetting = QString("dp/%1/apply").arg(i.key());
|
||||
if (settings->value(configsetting, "Manual").toString() == "Auto")
|
||||
i.value()->postProcess(ride);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
ManualDataProcessorDialog::ManualDataProcessorDialog(MainWindow *main, QString name, RideItem *ride) : main(main), ride(ride)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle(name);
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// find our processor
|
||||
const DataProcessorFactory &factory = DataProcessorFactory::instance();
|
||||
QMap<QString, DataProcessor*> processors = factory.getProcessors();
|
||||
processor = processors.value(name, NULL);
|
||||
|
||||
if (processor == NULL) reject();
|
||||
|
||||
QFont font;
|
||||
font.setWeight(QFont::Black);
|
||||
QLabel *configLabel = new QLabel(tr("Settings"), this);
|
||||
configLabel->setFont(font);
|
||||
QLabel *explainLabel = new QLabel(tr("Description"), this);
|
||||
explainLabel->setFont(font);
|
||||
|
||||
config = processor->processorConfig(this);
|
||||
config->readConfig();
|
||||
explain = new QTextEdit(this);
|
||||
explain->setText(config->explain());
|
||||
explain->setReadOnly(true);
|
||||
|
||||
mainLayout->addWidget(configLabel);
|
||||
mainLayout->addWidget(config);
|
||||
mainLayout->addWidget(explainLabel);
|
||||
mainLayout->addWidget(explain);
|
||||
|
||||
ok = new QPushButton(tr("OK"), this);
|
||||
cancel = new QPushButton(tr("Cancel"), this);
|
||||
QHBoxLayout *buttons = new QHBoxLayout();
|
||||
buttons->addStretch();
|
||||
buttons->addWidget(cancel);
|
||||
buttons->addWidget(ok);
|
||||
|
||||
mainLayout->addLayout(buttons);
|
||||
|
||||
connect(ok, SIGNAL(clicked()), this, SLOT(okClicked()));
|
||||
connect(cancel, SIGNAL(clicked()), this, SLOT(cancelClicked()));
|
||||
}
|
||||
|
||||
void
|
||||
ManualDataProcessorDialog::okClicked()
|
||||
{
|
||||
if (processor->postProcess((RideFile *)ride->ride(), config) == true) {
|
||||
main->notifyRideSelected(); // XXX to remain compatible with rest of GC for now
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
void
|
||||
ManualDataProcessorDialog::cancelClicked()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
114
src/DataProcessor.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _DataProcessor_h
|
||||
#define _DataProcessor_h
|
||||
|
||||
#include "RideFile.h"
|
||||
#include "RideFileCommand.h"
|
||||
#include "RideItem.h"
|
||||
#include <QDate>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
|
||||
// This file defines four classes:
|
||||
//
|
||||
// DataProcessorConfig is a base QWidget that must be supplied by the
|
||||
// DataProcessor to enable the user to configure its options
|
||||
//
|
||||
// DataProcessor is an abstract base class for function-objects that take a
|
||||
// rideFile and manipulate it. Examples include fixing gaps in recording or
|
||||
// creating the .notes or .cpi file
|
||||
//
|
||||
// DataProcessorFactory is a singleton that maintains a mapping of
|
||||
// all DataProcessor objects that can be applied to rideFiles
|
||||
//
|
||||
// ManualDataProcessorDialog is a dialog box to manually execute a
|
||||
// dataprocessor on the current ride and is called from the mainWindow menus
|
||||
//
|
||||
|
||||
|
||||
#include <QtGui>
|
||||
|
||||
// every data processor must supply a configuration Widget
|
||||
// when its processorConfig member is called
|
||||
class DataProcessorConfig : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DataProcessorConfig(QWidget *parent=0) : QWidget(parent) {}
|
||||
virtual ~DataProcessorConfig() {}
|
||||
virtual void readConfig() = 0;
|
||||
virtual void saveConfig() = 0;
|
||||
virtual QString explain() = 0;
|
||||
};
|
||||
|
||||
// the data processor abstract base class
|
||||
class DataProcessor
|
||||
{
|
||||
public:
|
||||
DataProcessor() {}
|
||||
virtual ~DataProcessor() {}
|
||||
virtual bool postProcess(RideFile *, DataProcessorConfig*settings=0) = 0;
|
||||
virtual DataProcessorConfig *processorConfig(QWidget *parent) = 0;
|
||||
};
|
||||
|
||||
// all data processors
|
||||
class DataProcessorFactory {
|
||||
|
||||
private:
|
||||
|
||||
static DataProcessorFactory *instance_;
|
||||
QMap<QString,DataProcessor*> processors;
|
||||
DataProcessorFactory() {}
|
||||
|
||||
public:
|
||||
|
||||
static DataProcessorFactory &instance();
|
||||
|
||||
bool registerProcessor(QString name, DataProcessor *processor);
|
||||
QMap<QString,DataProcessor*> getProcessors() const { return processors; }
|
||||
bool autoProcess(RideFile *); // run auto processes (after open rideFile)
|
||||
};
|
||||
|
||||
class MainWindow;
|
||||
class ManualDataProcessorDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ManualDataProcessorDialog(MainWindow *, QString, RideItem *);
|
||||
|
||||
private slots:
|
||||
void cancelClicked();
|
||||
void okClicked();
|
||||
|
||||
private:
|
||||
|
||||
MainWindow *main;
|
||||
RideItem *ride;
|
||||
DataProcessor *processor;
|
||||
DataProcessorConfig *config;
|
||||
QTextEdit *explain;
|
||||
QPushButton *ok, *cancel;
|
||||
};
|
||||
#endif // _DataProcessor_h
|
||||
127
src/FixGPS.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DataProcessor.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <algorithm>
|
||||
#include <QVector>
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
// Config widget used by the Preferences/Options config panes
|
||||
class FixGPS;
|
||||
class FixGPSConfig : public DataProcessorConfig
|
||||
{
|
||||
friend class ::FixGPS;
|
||||
protected:
|
||||
public:
|
||||
// there is no config
|
||||
FixGPSConfig(QWidget *parent) : DataProcessorConfig(parent) {}
|
||||
|
||||
QString explain() {
|
||||
return(QString(tr("Remove GPS errors and interpolate positional "
|
||||
"data where the GPS device did not record any data, "
|
||||
"or the data that was recorded is invalid.")));
|
||||
}
|
||||
|
||||
void readConfig() {}
|
||||
void saveConfig() {}
|
||||
};
|
||||
|
||||
|
||||
// RideFile Dataprocessor -- used to handle gaps in recording
|
||||
// by inserting interpolated/zero samples
|
||||
// to ensure dataPoints are contiguous in time
|
||||
//
|
||||
class FixGPS : public DataProcessor {
|
||||
|
||||
public:
|
||||
FixGPS() {}
|
||||
~FixGPS() {}
|
||||
|
||||
// the processor
|
||||
bool postProcess(RideFile *, DataProcessorConfig* config);
|
||||
|
||||
// the config widget
|
||||
DataProcessorConfig* processorConfig(QWidget *parent) {
|
||||
return new FixGPSConfig(parent);
|
||||
}
|
||||
};
|
||||
|
||||
static bool fixGPSAdded = DataProcessorFactory::instance().registerProcessor(QString(tr("Fix GPS errors")), new FixGPS());
|
||||
|
||||
bool
|
||||
FixGPS::postProcess(RideFile *ride, DataProcessorConfig *)
|
||||
{
|
||||
// ignore null or files without GPS data
|
||||
if (!ride || ride->areDataPresent()->lat == false || ride->areDataPresent()->lon == false)
|
||||
return false;
|
||||
|
||||
int errors=0;
|
||||
|
||||
ride->command->startLUW("Fix GPS Errors");
|
||||
|
||||
int lastgood = -1; // where did we last have decent GPS data?
|
||||
for (int i=0; i<ride->dataPoints().count(); i++) {
|
||||
// is this one decent?
|
||||
if (ride->dataPoints()[i]->lat && ride->dataPoints()[i]->lat >= double(-90) && ride->dataPoints()[i]->lat <= double(90) &&
|
||||
ride->dataPoints()[i]->lon && ride->dataPoints()[i]->lon >= double(-180) && ride->dataPoints()[i]->lon <= double(180)) {
|
||||
|
||||
if (lastgood != -1 && (lastgood+1) != i) {
|
||||
// interpolate from last good to here
|
||||
// then set last good to here
|
||||
double deltaLat = (ride->dataPoints()[i]->lat - ride->dataPoints()[lastgood]->lat) / double(i-lastgood);
|
||||
double deltaLon = (ride->dataPoints()[i]->lon - ride->dataPoints()[lastgood]->lon) / double(i-lastgood);
|
||||
for (int j=lastgood+1; j<i; j++) {
|
||||
ride->command->setPointValue(j, RideFile::lat, ride->dataPoints()[lastgood]->lat + (double(j-lastgood)*deltaLat));
|
||||
ride->command->setPointValue(j, RideFile::lon, ride->dataPoints()[lastgood]->lon + (double(j-lastgood)*deltaLon));
|
||||
errors++;
|
||||
}
|
||||
} else if (lastgood == -1) {
|
||||
// fill to front
|
||||
for (int j=0; j<i; j++) {
|
||||
ride->command->setPointValue(j, RideFile::lat, ride->dataPoints()[i]->lat);
|
||||
ride->command->setPointValue(j, RideFile::lon, ride->dataPoints()[i]->lon);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
lastgood = i;
|
||||
}
|
||||
}
|
||||
|
||||
// fill to end...
|
||||
if (lastgood != -1 && lastgood != (ride->dataPoints().count()-1)) {
|
||||
// fill from lastgood to end with lastgood
|
||||
for (int j=lastgood+1; j<ride->dataPoints().count(); j++) {
|
||||
ride->command->setPointValue(j, RideFile::lat, ride->dataPoints()[lastgood]->lat);
|
||||
ride->command->setPointValue(j, RideFile::lon, ride->dataPoints()[lastgood]->lon);
|
||||
errors++;
|
||||
}
|
||||
} else {
|
||||
// they are all bad!!
|
||||
// XXX do nothing?
|
||||
}
|
||||
ride->command->endLUW();
|
||||
|
||||
if (errors) {
|
||||
ride->setTag("GPS errors", QString("%1").arg(errors));
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
248
src/FixGaps.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DataProcessor.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <algorithm>
|
||||
#include <QVector>
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
// Config widget used by the Preferences/Options config panes
|
||||
class FixGaps;
|
||||
class FixGapsConfig : public DataProcessorConfig
|
||||
{
|
||||
friend class ::FixGaps;
|
||||
protected:
|
||||
QHBoxLayout *layout;
|
||||
QLabel *toleranceLabel, *beerandburritoLabel;
|
||||
QDoubleSpinBox *tolerance,
|
||||
*beerandburrito;
|
||||
|
||||
public:
|
||||
FixGapsConfig(QWidget *parent) : DataProcessorConfig(parent) {
|
||||
|
||||
layout = new QHBoxLayout(this);
|
||||
|
||||
layout->setContentsMargins(0,0,0,0);
|
||||
setContentsMargins(0,0,0,0);
|
||||
|
||||
toleranceLabel = new QLabel(tr("Tolerance"));
|
||||
beerandburritoLabel = new QLabel(tr("Stop"));
|
||||
|
||||
tolerance = new QDoubleSpinBox();
|
||||
tolerance->setMaximum(99.99);
|
||||
tolerance->setMinimum(0);
|
||||
tolerance->setSingleStep(0.1);
|
||||
|
||||
beerandburrito = new QDoubleSpinBox();
|
||||
beerandburrito->setMaximum(99999.99);
|
||||
beerandburrito->setMinimum(0);
|
||||
beerandburrito->setSingleStep(0.1);
|
||||
|
||||
layout->addWidget(toleranceLabel);
|
||||
layout->addWidget(tolerance);
|
||||
layout->addWidget(beerandburritoLabel);
|
||||
layout->addWidget(beerandburrito);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
//~FixGapsConfig() {} // deliberately not declared since Qt will delete
|
||||
// the widget and its children when the config pane is deleted
|
||||
|
||||
QString explain() {
|
||||
return(QString(tr("Many devices, especially wireless devices, will "
|
||||
"drop connections to the bike computer. This leads "
|
||||
"to lost samples in the resulting data, or so-called "
|
||||
"drops in recording.\n\n"
|
||||
"In order to calculate peak powers and averages, it "
|
||||
"is very helpful to remove these gaps, and either "
|
||||
"smooth the data where it is missing or just "
|
||||
"replace with zero value samples\n\n"
|
||||
"This function performs this task, taking two "
|
||||
"parameters;\n\n"
|
||||
"tolerance - this defines the minimum size of a "
|
||||
"recording gap (in seconds) that will be processed. "
|
||||
"any gap shorter than this will not be affected.\n\n"
|
||||
"stop - this defines the maximum size of "
|
||||
"gap (in seconds) that will have a smoothing algorithm "
|
||||
"applied. Where a gap is shorter than this value it will "
|
||||
"be filled with values interpolated from the values "
|
||||
"recorded before and after the gap. If it is longer "
|
||||
"than this value, it will be filled with zero values.")));
|
||||
}
|
||||
|
||||
void readConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
double tol = settings->value(GC_DPFG_TOLERANCE, "1.0").toDouble();
|
||||
double stop = settings->value(GC_DPFG_STOP, "1.0").toDouble();
|
||||
tolerance->setValue(tol);
|
||||
beerandburrito->setValue(stop);
|
||||
}
|
||||
|
||||
void saveConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
settings->setValue(GC_DPFG_TOLERANCE, tolerance->value());
|
||||
settings->setValue(GC_DPFG_STOP, beerandburrito->value());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// RideFile Dataprocessor -- used to handle gaps in recording
|
||||
// by inserting interpolated/zero samples
|
||||
// to ensure dataPoints are contiguous in time
|
||||
//
|
||||
class FixGaps : public DataProcessor {
|
||||
|
||||
public:
|
||||
FixGaps() {}
|
||||
~FixGaps() {}
|
||||
|
||||
// the processor
|
||||
bool postProcess(RideFile *, DataProcessorConfig* config);
|
||||
|
||||
// the config widget
|
||||
DataProcessorConfig* processorConfig(QWidget *parent) {
|
||||
return new FixGapsConfig(parent);
|
||||
}
|
||||
};
|
||||
|
||||
static bool fixGapsAdded = DataProcessorFactory::instance().registerProcessor(QString(tr("Fix Gaps in Recording")), new FixGaps());
|
||||
|
||||
bool
|
||||
FixGaps::postProcess(RideFile *ride, DataProcessorConfig *config=0)
|
||||
{
|
||||
// get settings
|
||||
double tolerance, stop;
|
||||
if (config == NULL) { // being called automatically
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
tolerance = settings->value(GC_DPFG_TOLERANCE, "1.0").toDouble();
|
||||
stop = settings->value(GC_DPFG_STOP, "1.0").toDouble();
|
||||
} else { // being called manually
|
||||
tolerance = ((FixGapsConfig*)(config))->tolerance->value();
|
||||
stop = ((FixGapsConfig*)(config))->beerandburrito->value();
|
||||
}
|
||||
|
||||
// if the number of duration / number of samples
|
||||
// equals the recording interval then we don't need
|
||||
// to post-process for gaps
|
||||
// XXX commented out since it is not always true and
|
||||
// is purely to improve performance
|
||||
//if ((ride->recIntSecs() + ride->dataPoints()[ride->dataPoints().count()-1]->secs -
|
||||
// ride->dataPoints()[0]->secs) / (double) ride->dataPoints().count() == ride->recIntSecs())
|
||||
// return false;
|
||||
|
||||
// Additionally, If there are less than 2 dataPoints then there
|
||||
// is no way of post processing anyway (e.g. manual workouts)
|
||||
if (ride->dataPoints().count() < 2) return false;
|
||||
|
||||
// OK, so there are probably some gaps, lets post process them
|
||||
RideFilePoint *last = NULL;
|
||||
int dropouts = 0;
|
||||
double dropouttime = 0.0;
|
||||
|
||||
// put it all in a LUW
|
||||
ride->command->startLUW("Fix Gaps in Recording");
|
||||
|
||||
for (int position = 0; position < ride->dataPoints().count(); position++) {
|
||||
RideFilePoint *point = ride->dataPoints()[position];
|
||||
|
||||
if (last == NULL) last = point;
|
||||
else {
|
||||
double gap = point->secs - last->secs - ride->recIntSecs();
|
||||
|
||||
// if we have gps and we moved, then this isn't a stop
|
||||
bool stationary = ((last->lat || last->lon) && (point->lat || point->lon)) // gps is present
|
||||
&& last->lat == point->lat && last->lon == point->lon;
|
||||
|
||||
// moved for less than 30 seconds ... interpolate
|
||||
if (!stationary && gap > tolerance && gap < stop) {
|
||||
|
||||
// what's needed?
|
||||
dropouts++;
|
||||
dropouttime += gap;
|
||||
|
||||
int count = gap/ride->recIntSecs();
|
||||
double hrdelta = (point->hr - last->hr) / (double) count;
|
||||
double pwrdelta = (point->watts - last->watts) / (double) count;
|
||||
double kphdelta = (point->kph - last->kph) / (double) count;
|
||||
double kmdelta = (point->km - last->km) / (double) count;
|
||||
double caddelta = (point->cad - last->cad) / (double) count;
|
||||
double altdelta = (point->alt - last->alt) / (double) count;
|
||||
double nmdelta = (point->nm - last->nm) / (double) count;
|
||||
double londelta = (point->lon - last->lon) / (double) count;
|
||||
double latdelta = (point->lat - last->lat) / (double) count;
|
||||
double hwdelta = (point->headwind - last->headwind) / (double) count;
|
||||
|
||||
// add the points
|
||||
for(int i=0; i<count; i++) {
|
||||
RideFilePoint *add = new RideFilePoint(last->secs+((i+1)*ride->recIntSecs()),
|
||||
last->cad+((i+1)*caddelta),
|
||||
last->hr + ((i+1)*hrdelta),
|
||||
last->km + ((i+1)*kmdelta),
|
||||
last->kph + ((i+1)*kphdelta),
|
||||
last->nm + ((i+1)*nmdelta),
|
||||
last->watts + ((i+1)*pwrdelta),
|
||||
last->alt + ((i+1)*altdelta),
|
||||
last->lon + ((i+1)*londelta),
|
||||
last->lat + ((i+1)*latdelta),
|
||||
last->headwind + ((i+1)*hwdelta),
|
||||
last->interval);
|
||||
ride->command->insertPoint(position++, add);
|
||||
}
|
||||
|
||||
// stationary or greater than 30 seconds... fill with zeroes
|
||||
} else if (gap > stop) {
|
||||
|
||||
dropouts++;
|
||||
dropouttime += gap;
|
||||
|
||||
int count = gap/ride->recIntSecs();
|
||||
double kmdelta = (point->km - last->km) / (double) count;
|
||||
|
||||
// add zero value points
|
||||
for(int i=0; i<count; i++) {
|
||||
RideFilePoint *add = new RideFilePoint(last->secs+((i+1)*ride->recIntSecs()),
|
||||
0,
|
||||
0,
|
||||
last->km + ((i+1)*kmdelta),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
last->interval);
|
||||
ride->command->insertPoint(position++, add);
|
||||
}
|
||||
}
|
||||
}
|
||||
last = point;
|
||||
}
|
||||
|
||||
// end the Logical unit of work here
|
||||
ride->command->endLUW();
|
||||
|
||||
ride->setTag("Dropouts", QString("%1").arg(dropouts));
|
||||
ride->setTag("Dropout Time", QString("%1").arg(dropouttime));
|
||||
|
||||
if (dropouts) return true;
|
||||
else return false;
|
||||
}
|
||||
198
src/FixSpikes.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DataProcessor.h"
|
||||
#include "LTMOutliers.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <algorithm>
|
||||
#include <QVector>
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
// Config widget used by the Preferences/Options config panes
|
||||
class FixSpikes;
|
||||
class FixSpikesConfig : public DataProcessorConfig
|
||||
{
|
||||
friend class ::FixSpikes;
|
||||
protected:
|
||||
QHBoxLayout *layout;
|
||||
QLabel *maxLabel, *varianceLabel;
|
||||
QDoubleSpinBox *max,
|
||||
*variance;
|
||||
|
||||
public:
|
||||
FixSpikesConfig(QWidget *parent) : DataProcessorConfig(parent) {
|
||||
|
||||
layout = new QHBoxLayout(this);
|
||||
|
||||
layout->setContentsMargins(0,0,0,0);
|
||||
setContentsMargins(0,0,0,0);
|
||||
|
||||
maxLabel = new QLabel(tr("Max"));
|
||||
varianceLabel = new QLabel(tr("Variance"));
|
||||
|
||||
max = new QDoubleSpinBox();
|
||||
max->setMaximum(9999.99);
|
||||
max->setMinimum(0);
|
||||
max->setSingleStep(1);
|
||||
|
||||
variance = new QDoubleSpinBox();
|
||||
variance->setMaximum(9999);
|
||||
variance->setMinimum(0);
|
||||
variance->setSingleStep(50);
|
||||
|
||||
layout->addWidget(maxLabel);
|
||||
layout->addWidget(max);
|
||||
layout->addWidget(varianceLabel);
|
||||
layout->addWidget(variance);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
//~FixSpikesConfig() {} // deliberately not declared since Qt will delete
|
||||
// the widget and its children when the config pane is deleted
|
||||
|
||||
QString explain() {
|
||||
return(QString(tr("Occasionally power meters will erroneously "
|
||||
"report high values for power. For crank based "
|
||||
"power meters such as SRM and Quarq this is "
|
||||
"caused by an erroneous cadence reading "
|
||||
"as a result of triggering a reed switch "
|
||||
"whilst pushing off\n\n"
|
||||
"This function will look for spikes/anomalies "
|
||||
"in power data and replace the erroneous data "
|
||||
"by smoothing/interpolating the data from either "
|
||||
"side of the point in question\n\n"
|
||||
"It takes the following parameters:\n\n"
|
||||
"Absolute Max - this defines an absolute value "
|
||||
"for watts, and will smooth any values above this "
|
||||
"absolute value that have been identified as being "
|
||||
"anomalies (i.e. at odds with the data surrounding it)\n\n"
|
||||
"Variance (%) - this will smooth any values which "
|
||||
"are higher than this percentage of the rolling "
|
||||
"average wattage for the 30 seconds leading up "
|
||||
"to the spike.")));
|
||||
}
|
||||
|
||||
void readConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
double tol = settings->value(GC_DPFS_MAX, "1500").toDouble();
|
||||
double stop = settings->value(GC_DPFS_VARIANCE, "1000").toDouble();
|
||||
max->setValue(tol);
|
||||
variance->setValue(stop);
|
||||
}
|
||||
|
||||
void saveConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
settings->setValue(GC_DPFS_MAX, max->value());
|
||||
settings->setValue(GC_DPFS_VARIANCE, variance->value());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// RideFile Dataprocessor -- used to handle gaps in recording
|
||||
// by inserting interpolated/zero samples
|
||||
// to ensure dataPoints are contiguous in time
|
||||
//
|
||||
class FixSpikes : public DataProcessor {
|
||||
|
||||
public:
|
||||
FixSpikes() {}
|
||||
~FixSpikes() {}
|
||||
|
||||
// the processor
|
||||
bool postProcess(RideFile *, DataProcessorConfig* config);
|
||||
|
||||
// the config widget
|
||||
DataProcessorConfig* processorConfig(QWidget *parent) {
|
||||
return new FixSpikesConfig(parent);
|
||||
}
|
||||
};
|
||||
|
||||
static bool fixSpikesAdded = DataProcessorFactory::instance().registerProcessor(QString(tr("Fix Power Spikes")), new FixSpikes());
|
||||
|
||||
bool
|
||||
FixSpikes::postProcess(RideFile *ride, DataProcessorConfig *config=0)
|
||||
{
|
||||
// does this ride have power?
|
||||
if (ride->areDataPresent()->watts == false) return false;
|
||||
|
||||
// get settings
|
||||
double variance, max;
|
||||
if (config == NULL) { // being called automatically
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
max = settings->value(GC_DPFS_MAX, "1500").toDouble();
|
||||
variance = settings->value(GC_DPFS_VARIANCE, "1000").toDouble();
|
||||
} else { // being called manually
|
||||
max = ((FixSpikesConfig*)(config))->max->value();
|
||||
variance = ((FixSpikesConfig*)(config))->variance->value();
|
||||
}
|
||||
|
||||
int windowsize = 30 / ride->recIntSecs();
|
||||
|
||||
// We use a window size of 30s to find spikes
|
||||
// if the ride is shorter, don't bother
|
||||
// is no way of post processing anyway (e.g. manual workouts)
|
||||
if (windowsize > ride->dataPoints().count()) return false;
|
||||
|
||||
// Find the power outliers
|
||||
|
||||
int spikes = 0;
|
||||
double spiketime = 0.0;
|
||||
|
||||
// create a data array for the outlier algorithm
|
||||
QVector<double> power;
|
||||
QVector<double> secs;
|
||||
|
||||
foreach (RideFilePoint *point, ride->dataPoints()) {
|
||||
power.append(point->watts);
|
||||
secs.append(point->secs);
|
||||
}
|
||||
|
||||
LTMOutliers *outliers = new LTMOutliers(secs.data(), power.data(), power.count(), windowsize, false);
|
||||
ride->command->startLUW("Fix Spikes in Recording");
|
||||
for (int i=0; i<secs.count(); i++) {
|
||||
|
||||
// is this over variance threshold?
|
||||
if (outliers->getDeviationForRank(i) < variance) break;
|
||||
|
||||
// ok, so its highly variant but is it over
|
||||
// the max value we are willing to accept?
|
||||
if (outliers->getYForRank(i) < max) continue;
|
||||
|
||||
// Houston, we have a spike
|
||||
spikes++;
|
||||
spiketime += ride->recIntSecs();
|
||||
|
||||
// which one is it
|
||||
int pos = outliers->getIndexForRank(i);
|
||||
double left=0.0, right=0.0;
|
||||
|
||||
if (pos > 0) left = ride->dataPoints()[pos-1]->watts;
|
||||
if (pos < (ride->dataPoints().count()-1)) right = ride->dataPoints()[pos+1]->watts;
|
||||
|
||||
ride->command->setPointValue(pos, RideFile::watts, (left+right)/2.0);
|
||||
}
|
||||
ride->command->endLUW();
|
||||
|
||||
ride->setTag("Spikes", QString("%1").arg(spikes));
|
||||
ride->setTag("Spike Time", QString("%1").arg(spiketime));
|
||||
|
||||
if (spikes) return true;
|
||||
else return false;
|
||||
}
|
||||
150
src/FixTorque.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DataProcessor.h"
|
||||
#include "LTMOutliers.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <algorithm>
|
||||
#include <QVector>
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
// Config widget used by the Preferences/Options config panes
|
||||
class FixTorque;
|
||||
class FixTorqueConfig : public DataProcessorConfig
|
||||
{
|
||||
friend class ::FixTorque;
|
||||
protected:
|
||||
QHBoxLayout *layout;
|
||||
QLabel *taLabel;
|
||||
QLineEdit *ta;
|
||||
|
||||
public:
|
||||
FixTorqueConfig(QWidget *parent) : DataProcessorConfig(parent) {
|
||||
|
||||
layout = new QHBoxLayout(this);
|
||||
|
||||
layout->setContentsMargins(0,0,0,0);
|
||||
setContentsMargins(0,0,0,0);
|
||||
|
||||
taLabel = new QLabel(tr("Torque Adjust"));
|
||||
|
||||
ta = new QLineEdit();
|
||||
|
||||
layout->addWidget(taLabel);
|
||||
layout->addWidget(ta);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
//~FixTorqueConfig() {} // deliberately not declared since Qt will delete
|
||||
// the widget and its children when the config pane is deleted
|
||||
|
||||
QString explain() {
|
||||
return(QString(tr("Adjusting torque values allows you to "
|
||||
"uplift or degrade the torque values when the calibration "
|
||||
"of your power meter was incorrect. It "
|
||||
"takes a single parameter:\n\n"
|
||||
"Torque Adjust - this defines an absolute value "
|
||||
"in poinds per square inch or newton meters to "
|
||||
"modify values by. Negative values are supported. (e.g. enter \"1.2 nm\" or "
|
||||
"\"-0.5 pi\").")));
|
||||
}
|
||||
|
||||
void readConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
ta->setText(settings->value(GC_DPTA, "0 nm").toString());
|
||||
}
|
||||
|
||||
void saveConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
settings->setValue(GC_DPTA, ta->text());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// RideFile Dataprocessor -- used to handle gaps in recording
|
||||
// by inserting interpolated/zero samples
|
||||
// to ensure dataPoints are contiguous in time
|
||||
//
|
||||
class FixTorque : public DataProcessor {
|
||||
|
||||
public:
|
||||
FixTorque() {}
|
||||
~FixTorque() {}
|
||||
|
||||
// the processor
|
||||
bool postProcess(RideFile *, DataProcessorConfig* config);
|
||||
|
||||
// the config widget
|
||||
DataProcessorConfig* processorConfig(QWidget *parent) {
|
||||
return new FixTorqueConfig(parent);
|
||||
}
|
||||
};
|
||||
|
||||
static bool fixTorqueAdded = DataProcessorFactory::instance().registerProcessor(QString(tr("Adjust Torque Values")), new FixTorque());
|
||||
|
||||
bool
|
||||
FixTorque::postProcess(RideFile *ride, DataProcessorConfig *config=0)
|
||||
{
|
||||
// does this ride have torque?
|
||||
if (ride->areDataPresent()->nm == false) return false;
|
||||
|
||||
// Lets do it then!
|
||||
QString ta;
|
||||
double nmAdjust;
|
||||
|
||||
if (config == NULL) { // being called automatically
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
ta = settings->value(GC_DPTA, "0 nm").toString();
|
||||
} else { // being called manually
|
||||
ta = ((FixTorqueConfig*)(config))->ta->text();
|
||||
}
|
||||
|
||||
// patrick's torque adjustment code
|
||||
bool pi = ta.endsWith("pi", Qt::CaseInsensitive);
|
||||
if (pi || ta.endsWith("nm", Qt::CaseInsensitive)) {
|
||||
nmAdjust = ta.left(ta.length() - 2).toDouble();
|
||||
if (pi) {
|
||||
nmAdjust *= 0.11298482933;
|
||||
}
|
||||
} else {
|
||||
nmAdjust = ta.toDouble();
|
||||
}
|
||||
|
||||
// no adjustment required
|
||||
if (nmAdjust == 0) return false;
|
||||
|
||||
// apply the change
|
||||
ride->command->startLUW("Adjust Torque");
|
||||
for (int i=0; i<ride->dataPoints().count(); i++) {
|
||||
RideFilePoint *point = ride->dataPoints()[i];
|
||||
|
||||
if (point->nm != 0) {
|
||||
double newnm = point->nm + nmAdjust;
|
||||
ride->command->setPointValue(i, RideFile::watts, point->watts * (newnm / point->nm));
|
||||
ride->command->setPointValue(i, RideFile::nm, newnm);
|
||||
}
|
||||
}
|
||||
ride->command->endLUW();
|
||||
|
||||
double currentta = ride->getTag("Torque Adjust", "0.0").toDouble();
|
||||
ride->setTag("Torque Adjust", QString("%1 nm").arg(currentta + nmAdjust));
|
||||
|
||||
return true;
|
||||
}
|
||||
93
src/LTMOutliers.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
#include <assert.h>
|
||||
#include "LTMOutliers.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
|
||||
LTMOutliers::LTMOutliers(double *xdata, double *ydata, int count, int windowsize, bool absolute) : stdDeviation(0.0)
|
||||
{
|
||||
double sum = 0;
|
||||
int points = 0;
|
||||
double allSum = 0.0;
|
||||
int pos=0;
|
||||
|
||||
assert(count >= windowsize);
|
||||
|
||||
// initial samples from point 0 to windowsize
|
||||
for (; pos < windowsize; ++pos) {
|
||||
|
||||
// we could either use a deviation of zero
|
||||
// or base it on what we have so far...
|
||||
// I chose to use sofar since spikes
|
||||
// are common at the start of a ride
|
||||
xdev add;
|
||||
add.x = xdata[pos];
|
||||
add.y = ydata[pos];
|
||||
add.pos = pos;
|
||||
|
||||
if (absolute) add.deviation = fabs(ydata[pos] - (sum/windowsize));
|
||||
else add.deviation = ydata[pos] - (sum/windowsize);
|
||||
|
||||
rank.append(add);
|
||||
|
||||
// when using -ve and +ve values stdDeviation is
|
||||
// based upon the absolute value of deviation
|
||||
// when not, we should only look at +ve values
|
||||
if ((!absolute && add.deviation > 0) || absolute) {
|
||||
allSum += add.deviation;
|
||||
points++;
|
||||
}
|
||||
sum += ydata[pos]; // initialise the moving average
|
||||
}
|
||||
|
||||
// bulk of samples from windowsize to the end
|
||||
for (; pos<count; pos++) {
|
||||
|
||||
// ranked list
|
||||
xdev add;
|
||||
add.x = xdata[pos];
|
||||
add.y = ydata[pos];
|
||||
add.pos = pos;
|
||||
if (absolute) add.deviation = fabs(ydata[pos] - (sum/windowsize));
|
||||
else add.deviation = ydata[pos] - (sum/windowsize);
|
||||
rank.append(add);
|
||||
|
||||
// calculate the sum for moving average
|
||||
sum += ydata[pos] - ydata[pos-windowsize];
|
||||
|
||||
// when using -ve and +ve values stdDeviation is
|
||||
// based upon the absolute value of deviation
|
||||
// when not, we should only look at +ve values
|
||||
if ((!absolute && add.deviation > 0) || absolute) {
|
||||
allSum += add.deviation;
|
||||
points++;
|
||||
}
|
||||
}
|
||||
|
||||
// and to the list of deviations
|
||||
// calculate the average deviation across all points
|
||||
stdDeviation = allSum / (double)points;
|
||||
|
||||
// create a ranked list
|
||||
qSort(rank);
|
||||
}
|
||||
56
src/LTMOutliers.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_LTMOutliers_h
|
||||
#define _GC_LTMOutliers_h 1
|
||||
|
||||
#include <QVector>
|
||||
#include <QMap>
|
||||
|
||||
class LTMOutliers
|
||||
{
|
||||
// used to produce a sorted list
|
||||
struct xdev {
|
||||
double x,y;
|
||||
int pos;
|
||||
double deviation;
|
||||
|
||||
bool operator< (xdev right) const {
|
||||
return (deviation > right.deviation); // sort ascending! (.gt not .lt)
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
// Constructor using arrays of x values and y values
|
||||
LTMOutliers(double *x, double *y, int count, int windowsize, bool absolute=true);
|
||||
|
||||
// ranked values
|
||||
int getIndexForRank(int i) { return rank[i].pos; }
|
||||
double getXForRank(int i) { return rank[i].x; }
|
||||
double getYForRank(int i) { return rank[i].y; }
|
||||
double getDeviationForRank(int i) { return rank[i].deviation; }
|
||||
|
||||
// std deviation
|
||||
double getStdDeviation() { return stdDeviation; }
|
||||
|
||||
protected:
|
||||
double stdDeviation;
|
||||
QVector<xdev> rank; // ranked list of x sorted by deviation
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "RealtimeWindow.h"
|
||||
#include "RideItem.h"
|
||||
#include "IntervalItem.h"
|
||||
#include "RideEditor.h"
|
||||
#include "RideFile.h"
|
||||
#include "RideSummaryWindow.h"
|
||||
#include "RideImportWizard.h"
|
||||
@@ -70,9 +71,6 @@
|
||||
#define GC_VERSION "(developer build)"
|
||||
#endif
|
||||
|
||||
#define FOLDER_TYPE 0
|
||||
#define RIDE_TYPE 1
|
||||
|
||||
bool
|
||||
MainWindow::parseRideFileName(const QString &name, QString *notesFileName, QDateTime *dt)
|
||||
{
|
||||
@@ -322,6 +320,11 @@ MainWindow::MainWindow(const QDir &home) :
|
||||
googleMap = new GoogleMapControl(this);
|
||||
tabs.append(TabInfo(googleMap, tr("Map")));
|
||||
|
||||
///////////////////////////// Editor //////////////////////////////////
|
||||
|
||||
rideEdit = new RideEditor(this);
|
||||
tabs.append(TabInfo(rideEdit, tr("Editor")));
|
||||
|
||||
////////////////////////////// Signals //////////////////////////////
|
||||
|
||||
connect(calendar, SIGNAL(clicked(const QDate &)),
|
||||
@@ -392,6 +395,29 @@ MainWindow::MainWindow(const QDir &home) :
|
||||
//optionsMenu->addAction(tr("&Update Metrics..."), this,
|
||||
// SLOT(scanForMissing()()), tr("Ctrl+U"));
|
||||
|
||||
// get the available processors
|
||||
const DataProcessorFactory &factory = DataProcessorFactory::instance();
|
||||
QMap<QString, DataProcessor*> processors = factory.getProcessors();
|
||||
|
||||
if (processors.count()) {
|
||||
|
||||
optionsMenu->addSeparator();
|
||||
toolMapper = new QSignalMapper(this); // maps each option
|
||||
QMapIterator<QString, DataProcessor*> i(processors);
|
||||
connect(toolMapper, SIGNAL(mapped(const QString &)), this, SLOT(manualProcess(const QString &)));
|
||||
|
||||
i.toFront();
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
QAction *action = new QAction(QString("%1...").arg(i.key()), this);
|
||||
optionsMenu->addAction(action);
|
||||
connect(action, SIGNAL(triggered()), toolMapper, SLOT(map()));
|
||||
toolMapper->setMapping(action, i.key());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
QMenu *viewMenu = menuBar()->addMenu(tr("&View"));
|
||||
QStringList tabsToHide = settings->value(GC_TABS_TO_HIDE, "").toString().split(",");
|
||||
for (int i = 0; i < tabs.size(); ++i) {
|
||||
@@ -507,7 +533,7 @@ MainWindow::addRide(QString name, bool bSelect /*=true*/)
|
||||
QTreeWidgetItem *item = allRides->child(index);
|
||||
if (item->type() != RIDE_TYPE)
|
||||
continue;
|
||||
RideItem *other = reinterpret_cast<RideItem*>(item);
|
||||
RideItem *other = static_cast<RideItem*>(item);
|
||||
|
||||
if(isAscending.toInt() > 0 ){
|
||||
if (other->dateTime > dt)
|
||||
@@ -541,7 +567,7 @@ MainWindow::removeCurrentRide()
|
||||
QTreeWidgetItem *_item = treeWidget->currentItem();
|
||||
if (_item->type() != RIDE_TYPE)
|
||||
return;
|
||||
RideItem *item = reinterpret_cast<RideItem*>(_item);
|
||||
RideItem *item = static_cast<RideItem*>(_item);
|
||||
|
||||
rideDeleted(item);
|
||||
|
||||
@@ -856,6 +882,9 @@ MainWindow::showTreeContextMenuPopup(const QPoint &pos)
|
||||
QAction *actSaveRide = new QAction(tr("Save Changes to Ride"), treeWidget);
|
||||
connect(actSaveRide, SIGNAL(triggered(void)), this, SLOT(saveRide()));
|
||||
|
||||
QAction *revertRide = new QAction(tr("Revert to Saved Ride"), treeWidget);
|
||||
connect(revertRide, SIGNAL(triggered(void)), this, SLOT(revertRide()));
|
||||
|
||||
QAction *actDeleteRide = new QAction(tr("Delete Ride"), treeWidget);
|
||||
connect(actDeleteRide, SIGNAL(triggered(void)), this, SLOT(deleteRide()));
|
||||
|
||||
@@ -870,8 +899,10 @@ MainWindow::showTreeContextMenuPopup(const QPoint &pos)
|
||||
|
||||
|
||||
|
||||
if (rideItem->isDirty() == true)
|
||||
if (rideItem->isDirty() == true) {
|
||||
menu.addAction(actSaveRide);
|
||||
menu.addAction(revertRide);
|
||||
}
|
||||
|
||||
menu.addAction(actDeleteRide);
|
||||
menu.addAction(actBestInt);
|
||||
@@ -1175,7 +1206,10 @@ void
|
||||
MainWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
if (saveRideExitDialog() == false) event->ignore();
|
||||
saveNotes();
|
||||
else {
|
||||
saveNotes();
|
||||
QApplication::clipboard()->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1295,6 +1329,17 @@ MainWindow::saveRide()
|
||||
saveRideSingleDialog(ride); // will update Dirty flag if saved
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::revertRide()
|
||||
{
|
||||
ride->freeMemory();
|
||||
ride->ride(); // force re-load
|
||||
|
||||
// in case reverted ride has different starttime
|
||||
ride->setStartTime(ride->ride()->startTime()); // Note: this will also signal rideSelected()
|
||||
ride->ride()->emitReverted();
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::splitRide()
|
||||
{
|
||||
@@ -1307,7 +1352,7 @@ MainWindow::deleteRide()
|
||||
QTreeWidgetItem *_item = treeWidget->currentItem();
|
||||
if (_item==NULL || _item->type() != RIDE_TYPE)
|
||||
return;
|
||||
RideItem *item = reinterpret_cast<RideItem*>(_item);
|
||||
RideItem *item = static_cast<RideItem*>(_item);
|
||||
QMessageBox msgBox;
|
||||
msgBox.setText(tr("Are you sure you want to delete the ride:"));
|
||||
msgBox.setInformativeText(item->fileName);
|
||||
@@ -1364,3 +1409,19 @@ MainWindow::notifyRideSelected()
|
||||
{
|
||||
rideSelected();
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::manualProcess(QString name)
|
||||
{
|
||||
// open a dialog box and let the users
|
||||
// configure the options to use
|
||||
// and also show the explanation
|
||||
// of what this function does
|
||||
// then call it!
|
||||
RideItem *rideitem = (RideItem*)currentRideItem();
|
||||
if (rideitem) {
|
||||
ManualDataProcessorDialog *p = new ManualDataProcessorDialog(this, name, rideitem);
|
||||
p->setWindowModality(Qt::ApplicationModal); // don't allow select other ride or it all goes wrong!
|
||||
p->exec();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class PerformanceManagerWindow;
|
||||
class RideSummaryWindow;
|
||||
class ViewSelection;
|
||||
class TrainWindow;
|
||||
class RideEditor;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
@@ -84,6 +85,8 @@ class MainWindow : public QMainWindow
|
||||
// signal emitted to notify its children
|
||||
void notifyRideSelected(); // used by RideItem to notify when
|
||||
// rideItem date/time changes
|
||||
void notifyRideClean() { rideClean(); }
|
||||
void notifyRideDirty() { rideDirty(); }
|
||||
void selectView(int);
|
||||
|
||||
// db connections to cyclistdir/metricDB - one per active MainWindow
|
||||
@@ -111,6 +114,8 @@ class MainWindow : public QMainWindow
|
||||
void viewChanged(int);
|
||||
void rideAdded(RideItem *);
|
||||
void rideDeleted(RideItem *);
|
||||
void rideDirty();
|
||||
void rideClean();
|
||||
|
||||
private slots:
|
||||
void tabViewTriggered(bool);
|
||||
@@ -125,6 +130,7 @@ class MainWindow : public QMainWindow
|
||||
void manualRide();
|
||||
void exportCSV();
|
||||
void exportGC();
|
||||
void manualProcess(QString);
|
||||
void importFile();
|
||||
void findBestIntervals();
|
||||
void addIntervalForPowerPeaksForSecs(RideFile *ride, int windowSizeSecs, QString name);
|
||||
@@ -135,6 +141,7 @@ class MainWindow : public QMainWindow
|
||||
void aboutDialog();
|
||||
void notesChanged();
|
||||
void saveRide(); // save current ride menu item
|
||||
void revertRide();
|
||||
bool saveRideExitDialog(); // save dirty rides on exit dialog
|
||||
void saveNotes();
|
||||
void showOptions();
|
||||
@@ -192,6 +199,7 @@ class MainWindow : public QMainWindow
|
||||
ModelWindow *modelWindow;
|
||||
AerolabWindow *aerolabWindow;
|
||||
GoogleMapControl *googleMap;
|
||||
RideEditor *rideEdit;
|
||||
QTreeWidgetItem *allRides;
|
||||
QTreeWidgetItem *allIntervals;
|
||||
QSplitter *leftLayout;
|
||||
@@ -218,6 +226,8 @@ class MainWindow : public QMainWindow
|
||||
bool useMetricUnits; // whether metric units are used (or imperial)
|
||||
|
||||
QuarqdClient *client;
|
||||
|
||||
QSignalMapper *toolMapper;
|
||||
};
|
||||
|
||||
#endif // _GC_MainWindow_h
|
||||
|
||||
@@ -843,10 +843,12 @@ MetadataPage::MetadataPage(MainWindow *main) : main(main)
|
||||
// setup maintenance pages using current config
|
||||
fieldsPage = new FieldsPage(this, fieldDefinitions);
|
||||
keywordsPage = new KeywordsPage(this, keywordDefinitions);
|
||||
processorPage = new ProcessorPage(main);
|
||||
|
||||
tabs = new QTabWidget(this);
|
||||
tabs->addTab(fieldsPage, tr("Fields"));
|
||||
tabs->addTab(keywordsPage, tr("Notes Keywords"));
|
||||
tabs->addTab(processorPage, tr("Processing"));
|
||||
|
||||
layout->addWidget(tabs);
|
||||
}
|
||||
@@ -860,6 +862,9 @@ MetadataPage::saveClicked()
|
||||
|
||||
// write to metadata.xml
|
||||
RideMetadata::serialize(main->home.absolutePath() + "/metadata.xml", keywordDefinitions, fieldDefinitions);
|
||||
|
||||
// save processors config
|
||||
processorPage->saveClicked();
|
||||
}
|
||||
|
||||
// little helper since we create/recreate combos
|
||||
@@ -1219,6 +1224,81 @@ FieldsPage::getDefinitions(QList<FieldDefinition> &fieldList)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Data processors config page
|
||||
//
|
||||
ProcessorPage::ProcessorPage(MainWindow *main) : main(main)
|
||||
{
|
||||
// get the available processors
|
||||
const DataProcessorFactory &factory = DataProcessorFactory::instance();
|
||||
processors = factory.getProcessors();
|
||||
|
||||
QGridLayout *mainLayout = new QGridLayout(this);
|
||||
|
||||
processorTree = new QTreeWidget;
|
||||
processorTree->headerItem()->setText(0, tr("Processor"));
|
||||
processorTree->headerItem()->setText(1, tr("Apply"));
|
||||
processorTree->headerItem()->setText(2, tr("Settings"));
|
||||
processorTree->setColumnCount(3);
|
||||
processorTree->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
processorTree->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit
|
||||
processorTree->setUniformRowHeights(true);
|
||||
processorTree->setIndentation(0);
|
||||
processorTree->header()->resizeSection(0,150);
|
||||
|
||||
// iterate over all the processors and add an entry to the
|
||||
QMapIterator<QString, DataProcessor*> i(processors);
|
||||
i.toFront();
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QTreeWidgetItem *add;
|
||||
|
||||
add = new QTreeWidgetItem(processorTree->invisibleRootItem());
|
||||
add->setFlags(add->flags() & ~Qt::ItemIsEditable);
|
||||
|
||||
// Processor Name
|
||||
add->setText(0, i.key());
|
||||
|
||||
// Auto or Manual run?
|
||||
QComboBox *comboButton = new QComboBox(this);
|
||||
comboButton->addItem(tr("Manual"));
|
||||
comboButton->addItem(tr("Auto"));
|
||||
processorTree->setItemWidget(add, 1, comboButton);
|
||||
|
||||
QString configsetting = QString("dp/%1/apply").arg(i.key());
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
if (settings->value(configsetting, "Manual").toString() == "Manual")
|
||||
comboButton->setCurrentIndex(0);
|
||||
else
|
||||
comboButton->setCurrentIndex(1);
|
||||
|
||||
// Get and Set the Config Widget
|
||||
DataProcessorConfig *config = i.value()->processorConfig(this);
|
||||
config->readConfig();
|
||||
|
||||
processorTree->setItemWidget(add, 2, config);
|
||||
}
|
||||
|
||||
mainLayout->addWidget(processorTree, 0,0);
|
||||
}
|
||||
|
||||
void
|
||||
ProcessorPage::saveClicked()
|
||||
{
|
||||
// call each processor config widget's saveConfig() to
|
||||
// write away separately
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
for (int i=0; i<processorTree->invisibleRootItem()->childCount(); i++) {
|
||||
// auto or manual?
|
||||
QString configsetting = QString("dp/%1/apply").arg(processorTree->invisibleRootItem()->child(i)->text(0));
|
||||
QString apply = ((QComboBox*)(processorTree->itemWidget(processorTree->invisibleRootItem()->child(i), 1)))->currentIndex() ?
|
||||
"Auto" : "Manual";
|
||||
settings->setValue(configsetting, apply);
|
||||
((DataProcessorConfig*)(processorTree->itemWidget(processorTree->invisibleRootItem()->child(i), 2)))->saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Zone Config page
|
||||
//
|
||||
|
||||
26
src/Pages.h
@@ -21,6 +21,7 @@
|
||||
#include "DeviceTypes.h"
|
||||
#include "DeviceConfiguration.h"
|
||||
#include "RideMetadata.h"
|
||||
#include "DataProcessor.h"
|
||||
|
||||
class QGroupBox;
|
||||
class QHBoxLayout;
|
||||
@@ -265,6 +266,30 @@ class FieldsPage : public QWidget
|
||||
QPushButton *upButton, *downButton, *addButton, *renameButton, *deleteButton;
|
||||
};
|
||||
|
||||
class ProcessorPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
ProcessorPage(MainWindow *main);
|
||||
void saveClicked();
|
||||
|
||||
public slots:
|
||||
|
||||
//void upClicked();
|
||||
//void downClicked();
|
||||
|
||||
protected:
|
||||
|
||||
MainWindow *main;
|
||||
QMap<QString, DataProcessor*> processors;
|
||||
|
||||
QTreeWidget *processorTree;
|
||||
//QPushButton *upButton, *downButton;
|
||||
|
||||
};
|
||||
|
||||
class MetadataPage : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -285,6 +310,7 @@ class MetadataPage : public QWidget
|
||||
QTabWidget *tabs;
|
||||
KeywordsPage *keywordsPage;
|
||||
FieldsPage *fieldsPage;
|
||||
ProcessorPage *processorPage;
|
||||
|
||||
// local versions for modification
|
||||
QList<KeywordDefinition> keywordDefinitions;
|
||||
|
||||
2241
src/RideEditor.cpp
Normal file
271
src/RideEditor.h
Normal file
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_RideEditor_h
|
||||
#define _GC_RideEditor_h 1
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "RideItem.h"
|
||||
#include "RideFile.h"
|
||||
#include "RideFileCommand.h"
|
||||
#include "RideFileTableModel.h"
|
||||
#include <QtGui>
|
||||
|
||||
class EditorData;
|
||||
class CellDelegate;
|
||||
class RideModel;
|
||||
class FindDialog;
|
||||
class PasteSpecialDialog;
|
||||
|
||||
class RideEditor : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class ::FindDialog;
|
||||
friend class ::PasteSpecialDialog;
|
||||
friend class ::CellDelegate;
|
||||
|
||||
public:
|
||||
|
||||
RideEditor(MainWindow *);
|
||||
|
||||
// item delegate uses this
|
||||
QTableView *table;
|
||||
|
||||
// read/write the model
|
||||
void setModelValue(int row, int column, double value);
|
||||
double getValue(int row, int column);
|
||||
|
||||
// setup context menu
|
||||
void stdContextMenu (QMenu *, const QPoint &pos);
|
||||
bool isAnomaly(int x, int y);
|
||||
bool isEdited(int x, int y);
|
||||
bool isFound(int x, int y);
|
||||
bool isTooPrecise(int x, int y);
|
||||
bool isRowSelected();
|
||||
bool isColumnSelected();
|
||||
|
||||
signals:
|
||||
void insertRows();
|
||||
void insertColumns();
|
||||
void deleteRows();
|
||||
void deleteColumns();
|
||||
|
||||
public slots:
|
||||
|
||||
// toolbar functions
|
||||
void saveFile();
|
||||
void undo();
|
||||
void redo();
|
||||
void find();
|
||||
void check();
|
||||
|
||||
// context menu functions
|
||||
void smooth();
|
||||
void cut();
|
||||
void delColumn();
|
||||
void insColumn(QString);
|
||||
void delRow();
|
||||
void insRow();
|
||||
void copy();
|
||||
void paste();
|
||||
void pasteSpecial();
|
||||
void clear();
|
||||
|
||||
// trap QTableView signals
|
||||
bool eventFilter(QObject *, QEvent *);
|
||||
void cellMenu(const QPoint &);
|
||||
void borderMenu(const QPoint &);
|
||||
|
||||
// ride command
|
||||
void beginCommand(bool,RideCommand*);
|
||||
void endCommand(bool,RideCommand*);
|
||||
|
||||
// GC signals
|
||||
void configChanged();
|
||||
void rideSelected();
|
||||
void intervalSelected();
|
||||
void rideDirty();
|
||||
void rideClean();
|
||||
|
||||
// util
|
||||
void getPaste(QVector<QVector<double> >&cells,
|
||||
QStringList &seps, QStringList &head, bool);
|
||||
|
||||
protected:
|
||||
EditorData *data;
|
||||
RideItem *ride;
|
||||
RideFileTableModel *model;
|
||||
QStringList copyHeadings;
|
||||
|
||||
private:
|
||||
MainWindow *main;
|
||||
|
||||
bool inLUW;
|
||||
QList<QModelIndex> itemselection;
|
||||
|
||||
double DPFSmax, DPFSvariance;
|
||||
|
||||
QList<QString> whatColumns();
|
||||
QSignalMapper *colMapper;
|
||||
|
||||
QToolBar *toolbar;
|
||||
QAction *saveAct, *undoAct, *redoAct,
|
||||
*searchAct, *checkAct;
|
||||
|
||||
// state data
|
||||
struct { int row, column; } currentCell;
|
||||
};
|
||||
|
||||
class EditorData
|
||||
{
|
||||
public:
|
||||
QMap<QString, QString> anomalies;
|
||||
QMap<QString, QString> found;
|
||||
|
||||
// when underlying data is modified
|
||||
// these are called to adjust references
|
||||
void deleteRows(int row, int count);
|
||||
void insertRows(int row, int count);
|
||||
void deleteSeries(RideFile::SeriesType);
|
||||
};
|
||||
|
||||
class RideModel : public QStandardItemModel
|
||||
{
|
||||
public:
|
||||
RideModel(int rows, int columns) : QStandardItemModel(rows, columns) {}
|
||||
void forceRedraw(const QModelIndex&x, const QModelIndex&y) { emit dataChanged(x,y); }
|
||||
};
|
||||
|
||||
class CellDelegate : public QItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CellDelegate(RideEditor *, QObject *parent = 0);
|
||||
|
||||
// setup editor in the cell - QDoubleSpinBox
|
||||
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
|
||||
// Fetch data from model ready for editing
|
||||
void setEditorData(QWidget *editor, const QModelIndex &index) const;
|
||||
|
||||
// Save data back to model after editing
|
||||
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const;
|
||||
|
||||
// override stanard painter to underline anomalies in red
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
|
||||
void updateEditorGeometry(QWidget *editor,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const;
|
||||
|
||||
private slots:
|
||||
|
||||
void commitAndCloseEditor();
|
||||
|
||||
private:
|
||||
RideEditor *rideEditor;
|
||||
|
||||
};
|
||||
|
||||
//
|
||||
// Dialog for finding values across the ride
|
||||
//
|
||||
class FindDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
FindDialog(RideEditor *, QWidget *parent=0);
|
||||
~FindDialog();
|
||||
|
||||
private slots:
|
||||
void find();
|
||||
void close();
|
||||
void selection();
|
||||
void typeChanged(int);
|
||||
void dataChanged();
|
||||
|
||||
private:
|
||||
RideEditor *rideEditor;
|
||||
|
||||
QComboBox *type;
|
||||
QDoubleSpinBox *from, *to;
|
||||
QLabel *andLabel;
|
||||
|
||||
QList<QCheckBox*> channels;
|
||||
QPushButton *findButton, *closeButton;
|
||||
QTableWidget *resultsTable;
|
||||
|
||||
void clearResultsTable();
|
||||
};
|
||||
|
||||
//
|
||||
// Dialog for paste special
|
||||
//
|
||||
class PasteSpecialDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
PasteSpecialDialog(RideEditor *, QWidget *parent=0);
|
||||
~PasteSpecialDialog();
|
||||
|
||||
private slots:
|
||||
void okClicked();
|
||||
void cancelClicked();
|
||||
|
||||
void setColumnSelect();
|
||||
void columnChanged();
|
||||
void sepsChanged();
|
||||
|
||||
private:
|
||||
RideEditor *rideEditor;
|
||||
bool active;
|
||||
|
||||
QStringList seps;
|
||||
QVector<QVector<double> > cells;
|
||||
QStandardItemModel *model;
|
||||
QStringList headings; // the header values
|
||||
QStringList sourceHeadings; // source header values
|
||||
|
||||
// Group boxes
|
||||
QGroupBox *mode, *separators, *contents;
|
||||
|
||||
// insert/overwrite
|
||||
QRadioButton *append, *overwrite;
|
||||
QDoubleSpinBox *atRow;
|
||||
|
||||
// separator options
|
||||
QCheckBox *tab, *comma, *semi, *space, *other;
|
||||
QLineEdit *otherText;
|
||||
QComboBox *textDelimeter;
|
||||
QCheckBox *hasHeader;
|
||||
|
||||
// table selection
|
||||
QComboBox *columnSelect;
|
||||
QTableView *resultsTable;
|
||||
void setResultsTable();
|
||||
void clearResultsTable();
|
||||
|
||||
QPushButton *okButton, *cancelButton;
|
||||
};
|
||||
#endif // _GC_RideEditor_h
|
||||
243
src/RideFile.cpp
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "RideFile.h"
|
||||
#include "DataProcessor.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <QtXml/QtXml>
|
||||
@@ -31,6 +32,49 @@
|
||||
interval = point->interval; \
|
||||
start = point->secs; \
|
||||
}
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
RideFile::RideFile(const QDateTime &startTime, double recIntSecs) :
|
||||
startTime_(startTime), recIntSecs_(recIntSecs),
|
||||
deviceType_("unknown"), data(NULL)
|
||||
{
|
||||
command = new RideFileCommand(this);
|
||||
}
|
||||
|
||||
RideFile::RideFile() : recIntSecs_(0.0), deviceType_("unknown"), data(NULL)
|
||||
{
|
||||
command = new RideFileCommand(this);
|
||||
}
|
||||
|
||||
RideFile::~RideFile()
|
||||
{
|
||||
foreach(RideFilePoint *point, dataPoints_)
|
||||
delete point;
|
||||
delete command;
|
||||
}
|
||||
|
||||
QString
|
||||
RideFile::seriesName(SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
case RideFile::secs: return QString(tr("Time"));
|
||||
case RideFile::cad: return QString(tr("Cadence"));
|
||||
case RideFile::hr: return QString(tr("Heartrate"));
|
||||
case RideFile::km: return QString(tr("Distance"));
|
||||
case RideFile::kph: return QString(tr("Speed"));
|
||||
case RideFile::nm: return QString(tr("Torque"));
|
||||
case RideFile::watts: return QString(tr("Power"));
|
||||
case RideFile::alt: return QString(tr("Altitude"));
|
||||
case RideFile::lon: return QString(tr("Longitude"));
|
||||
case RideFile::lat: return QString(tr("Latitude"));
|
||||
case RideFile::headwind: return QString(tr("Headwind"));
|
||||
case RideFile::interval: return QString(tr("Interval"));
|
||||
default: return QString(tr("Unknown"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RideFile::clearIntervals()
|
||||
{
|
||||
@@ -206,8 +250,12 @@ RideFile *RideFileFactory::openRideFile(QFile &file,
|
||||
RideFileReader *reader = readFuncs_.value(suffix.toLower());
|
||||
assert(reader);
|
||||
RideFile *result = reader->openRideFile(file, errors);
|
||||
if (result && result->intervals().empty())
|
||||
result->fillInIntervals();
|
||||
|
||||
if (result->intervals().empty()) result->fillInIntervals();
|
||||
|
||||
result->setTag("Filename", file.fileName());
|
||||
DataProcessorFactory::instance().autoProcess(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -220,8 +268,7 @@ QStringList RideFileFactory::listRideFiles(const QDir &dir) const
|
||||
filters << ("*." + i.key());
|
||||
}
|
||||
// This will read the user preferences and change the file list order as necessary:
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();;
|
||||
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
QVariant isAscending = settings->value(GC_ALLRIDES_ASCENDING,Qt::Checked);
|
||||
if(isAscending.toInt()>0){
|
||||
return dir.entryList(filters, QDir::Files, QDir::Name);
|
||||
@@ -248,3 +295,191 @@ void RideFile::appendPoint(double secs, double cad, double hr, double km,
|
||||
dataPresent.headwind |= (headwind != 0);
|
||||
dataPresent.interval |= (interval != 0);
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::setDataPresent(SeriesType series, bool value)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : dataPresent.secs = value; break;
|
||||
case cad : dataPresent.cad = value; break;
|
||||
case hr : dataPresent.hr = value; break;
|
||||
case km : dataPresent.km = value; break;
|
||||
case kph : dataPresent.kph = value; break;
|
||||
case nm : dataPresent.nm = value; break;
|
||||
case watts : dataPresent.watts = value; break;
|
||||
case alt : dataPresent.alt = value; break;
|
||||
case lon : dataPresent.lon = value; break;
|
||||
case lat : dataPresent.lat = value; break;
|
||||
case headwind : dataPresent.headwind = value; break;
|
||||
case interval : dataPresent.interval = value; break;
|
||||
case none : break;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
RideFile::isDataPresent(SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : return dataPresent.secs; break;
|
||||
case cad : return dataPresent.cad; break;
|
||||
case hr : return dataPresent.hr; break;
|
||||
case km : return dataPresent.km; break;
|
||||
case kph : return dataPresent.kph; break;
|
||||
case nm : return dataPresent.nm; break;
|
||||
case watts : return dataPresent.watts; break;
|
||||
case alt : return dataPresent.alt; break;
|
||||
case lon : return dataPresent.lon; break;
|
||||
case lat : return dataPresent.lat; break;
|
||||
case headwind : return dataPresent.headwind; break;
|
||||
case interval : return dataPresent.interval; break;
|
||||
case none : break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void
|
||||
RideFile::setPointValue(int index, SeriesType series, double value)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : dataPoints_[index]->secs = value; break;
|
||||
case cad : dataPoints_[index]->cad = value; break;
|
||||
case hr : dataPoints_[index]->hr = value; break;
|
||||
case km : dataPoints_[index]->km = value; break;
|
||||
case kph : dataPoints_[index]->kph = value; break;
|
||||
case nm : dataPoints_[index]->nm = value; break;
|
||||
case watts : dataPoints_[index]->watts = value; break;
|
||||
case alt : dataPoints_[index]->alt = value; break;
|
||||
case lon : dataPoints_[index]->lon = value; break;
|
||||
case lat : dataPoints_[index]->lat = value; break;
|
||||
case headwind : dataPoints_[index]->headwind = value; break;
|
||||
case interval : dataPoints_[index]->interval = value; break;
|
||||
case none : break;
|
||||
}
|
||||
}
|
||||
|
||||
double
|
||||
RideFile::getPointValue(int index, SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : return dataPoints_[index]->secs; break;
|
||||
case cad : return dataPoints_[index]->cad; break;
|
||||
case hr : return dataPoints_[index]->hr; break;
|
||||
case km : return dataPoints_[index]->km; break;
|
||||
case kph : return dataPoints_[index]->kph; break;
|
||||
case nm : return dataPoints_[index]->nm; break;
|
||||
case watts : return dataPoints_[index]->watts; break;
|
||||
case alt : return dataPoints_[index]->alt; break;
|
||||
case lon : return dataPoints_[index]->lon; break;
|
||||
case lat : return dataPoints_[index]->lat; break;
|
||||
case headwind : return dataPoints_[index]->headwind; break;
|
||||
case interval : return dataPoints_[index]->interval; break;
|
||||
case none : break;
|
||||
}
|
||||
return 0.0; // shutup the compiler
|
||||
}
|
||||
|
||||
int
|
||||
RideFile::decimalsFor(SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : return 3; break;
|
||||
case cad : return 0; break;
|
||||
case hr : return 0; break;
|
||||
case km : return 6; break;
|
||||
case kph : return 4; break;
|
||||
case nm : return 2; break;
|
||||
case watts : return 0; break;
|
||||
case alt : return 3; break;
|
||||
case lon : return 6; break;
|
||||
case lat : return 6; break;
|
||||
case headwind : return 4; break;
|
||||
case interval : return 0; break;
|
||||
case none : break;
|
||||
}
|
||||
return 2; // default
|
||||
}
|
||||
|
||||
double
|
||||
RideFile::maximumFor(SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : return 999999; break;
|
||||
case cad : return 300; break;
|
||||
case hr : return 300; break;
|
||||
case km : return 999999; break;
|
||||
case kph : return 999; break;
|
||||
case nm : return 999; break;
|
||||
case watts : return 4000; break;
|
||||
case alt : return 8850; break; // mt everest is highest point above sea level
|
||||
case lon : return 180; break;
|
||||
case lat : return 90; break;
|
||||
case headwind : return 999; break;
|
||||
case interval : return 999; break;
|
||||
case none : break;
|
||||
}
|
||||
return 9999; // default
|
||||
}
|
||||
|
||||
double
|
||||
RideFile::minimumFor(SeriesType series)
|
||||
{
|
||||
switch (series) {
|
||||
case secs : return 0; break;
|
||||
case cad : return 0; break;
|
||||
case hr : return 0; break;
|
||||
case km : return 0; break;
|
||||
case kph : return 0; break;
|
||||
case nm : return 0; break;
|
||||
case watts : return 0; break;
|
||||
case alt : return -413; break; // the Red Sea is lowest land point on earth
|
||||
case lon : return -180; break;
|
||||
case lat : return -90; break;
|
||||
case headwind : return -999; break;
|
||||
case interval : return 0; break;
|
||||
case none : break;
|
||||
}
|
||||
return 0; // default
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::deletePoint(int index)
|
||||
{
|
||||
delete dataPoints_[index];
|
||||
dataPoints_.remove(index);
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::deletePoints(int index, int count)
|
||||
{
|
||||
for(int i=index; i<(index+count); i++) delete dataPoints_[i];
|
||||
dataPoints_.remove(index, count);
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::insertPoint(int index, RideFilePoint *point)
|
||||
{
|
||||
dataPoints_.insert(index, point);
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::appendPoints(QVector <struct RideFilePoint *> newRows)
|
||||
{
|
||||
dataPoints_ += newRows;
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::emitSaved()
|
||||
{
|
||||
emit saved();
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::emitReverted()
|
||||
{
|
||||
emit reverted();
|
||||
}
|
||||
|
||||
void
|
||||
RideFile::emitModified()
|
||||
{
|
||||
emit modified();
|
||||
}
|
||||
|
||||
113
src/RideFile.h
@@ -25,6 +25,11 @@
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
#include <QObject>
|
||||
|
||||
class RideItem;
|
||||
class EditorData; // attached to a RideFile
|
||||
class RideFileCommand; // for manipulating ride data
|
||||
|
||||
// This file defines four classes:
|
||||
//
|
||||
@@ -71,43 +76,50 @@ struct RideFileInterval
|
||||
start(start), stop(stop), name(name) {}
|
||||
};
|
||||
|
||||
class RideFile
|
||||
class RideFile : public QObject // QObject to emit signals
|
||||
{
|
||||
private:
|
||||
|
||||
QDateTime startTime_; // time of day that the ride started
|
||||
double recIntSecs_; // recording interval in seconds
|
||||
QVector<RideFilePoint*> dataPoints_;
|
||||
RideFileDataPresent dataPresent;
|
||||
QString deviceType_;
|
||||
QList<RideFileInterval> intervals_;
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
RideFile() : recIntSecs_(0.0), deviceType_("unknown") {}
|
||||
RideFile(const QDateTime &startTime, double recIntSecs) :
|
||||
startTime_(startTime), recIntSecs_(recIntSecs),
|
||||
deviceType_("unknown") {}
|
||||
friend class RideFileCommand; // tells us we were modified
|
||||
friend class MainWindow; // tells us we were saved
|
||||
|
||||
virtual ~RideFile() {
|
||||
foreach(RideFilePoint *point, dataPoints_)
|
||||
delete point;
|
||||
}
|
||||
// Constructor / Destructor
|
||||
RideFile();
|
||||
RideFile(const QDateTime &startTime, double recIntSecs);
|
||||
virtual ~RideFile();
|
||||
|
||||
const QDateTime &startTime() const { return startTime_; }
|
||||
double recIntSecs() const { return recIntSecs_; }
|
||||
const QVector<RideFilePoint*> dataPoints() const { return dataPoints_; }
|
||||
inline const RideFileDataPresent *areDataPresent() const { return &dataPresent; }
|
||||
const QString &deviceType() const { return deviceType_; }
|
||||
|
||||
void setStartTime(const QDateTime &value) { startTime_ = value; }
|
||||
void setRecIntSecs(double value) { recIntSecs_ = value; }
|
||||
void setDeviceType(const QString &value) { deviceType_ = value; }
|
||||
// Working with DATASERIES
|
||||
enum seriestype { secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, interval, none };
|
||||
typedef enum seriestype SeriesType;
|
||||
static QString seriesName(SeriesType);
|
||||
static int decimalsFor(SeriesType series);
|
||||
static double maximumFor(SeriesType series);
|
||||
static double minimumFor(SeriesType series);
|
||||
|
||||
// Working with DATAPOINTS -- ***use command to modify***
|
||||
RideFileCommand *command;
|
||||
double getPointValue(int index, SeriesType series);
|
||||
void appendPoint(double secs, double cad, double hr, double km,
|
||||
double kph, double nm, double watts, double alt,
|
||||
double lon, double lat, double headwind, int interval);
|
||||
const QVector<RideFilePoint*> &dataPoints() const { return dataPoints_; }
|
||||
|
||||
// Working with DATAPRESENT flags
|
||||
inline const RideFileDataPresent *areDataPresent() const { return &dataPresent; }
|
||||
void resetDataPresent();
|
||||
bool isDataPresent(SeriesType series);
|
||||
|
||||
// Working with FIRST CLASS variables
|
||||
const QDateTime &startTime() const { return startTime_; }
|
||||
void setStartTime(const QDateTime &value) { startTime_ = value; }
|
||||
double recIntSecs() const { return recIntSecs_; }
|
||||
void setRecIntSecs(double value) { recIntSecs_ = value; }
|
||||
const QString &deviceType() const { return deviceType_; }
|
||||
void setDeviceType(const QString &value) { deviceType_ = value; }
|
||||
|
||||
// Working with INTERVALS
|
||||
const QList<RideFileInterval> &intervals() const { return intervals_; }
|
||||
void addInterval(double start, double stop, const QString &name) {
|
||||
intervals_.append(RideFileInterval(start, stop, name));
|
||||
@@ -118,18 +130,60 @@ class RideFile
|
||||
|
||||
void writeAsCsv(QFile &file, bool bIsMetric) const;
|
||||
|
||||
void resetDataPresent();
|
||||
|
||||
// Index offset calculations
|
||||
double timeToDistance(double) const; // get distance in km at time in secs
|
||||
int timeIndex(double) const; // get index offset for time in secs
|
||||
int distanceIndex(double) const; // get index offset for distance in KM
|
||||
|
||||
// Working with the METADATA TAGS
|
||||
const QMap<QString,QString>& tags() const { return tags_; }
|
||||
QString getTag(QString name, QString fallback) { return tags_.value(name, fallback); }
|
||||
QString getTag(QString name, QString fallback) const { return tags_.value(name, fallback); }
|
||||
void setTag(QString name, QString value) { tags_.insert(name, value); }
|
||||
|
||||
// METRIC OVERRIDES
|
||||
QMap<QString,QMap<QString,QString> > metricOverrides;
|
||||
|
||||
// editor data is held here and updated
|
||||
// as rows/columns are added/removed
|
||||
// this is a workaround to avoid holding
|
||||
// state data within each RideFilePoint structure
|
||||
// and avoiding impact on existing code
|
||||
EditorData *editorData() { return data; }
|
||||
void setEditorData(EditorData *x) { data = x; }
|
||||
|
||||
// ************************ WARNING ***************************
|
||||
// you shouldn't use these routines directly
|
||||
// rather use the RideFileCommand *command
|
||||
// to manipulate the ride data
|
||||
void setPointValue(int index, SeriesType series, double value);
|
||||
void deletePoint(int index);
|
||||
void deletePoints(int index, int count);
|
||||
void insertPoint(int index, RideFilePoint *point);
|
||||
void appendPoints(QVector <struct RideFilePoint *> newRows);
|
||||
void setDataPresent(SeriesType, bool);
|
||||
// ************************************************************
|
||||
|
||||
signals:
|
||||
void saved();
|
||||
void reverted();
|
||||
void modified();
|
||||
|
||||
protected:
|
||||
void emitSaved();
|
||||
void emitReverted();
|
||||
void emitModified();
|
||||
|
||||
private:
|
||||
|
||||
QDateTime startTime_; // time of day that the ride started
|
||||
double recIntSecs_; // recording interval in seconds
|
||||
QVector<RideFilePoint*> dataPoints_;
|
||||
RideFileDataPresent dataPresent;
|
||||
QString deviceType_;
|
||||
QList<RideFileInterval> intervals_;
|
||||
QMap<QString,QString> tags_;
|
||||
EditorData *data;
|
||||
|
||||
};
|
||||
|
||||
struct RideFileReader {
|
||||
@@ -163,4 +217,3 @@ class RideFileFactory {
|
||||
};
|
||||
|
||||
#endif // _RideFile_h
|
||||
|
||||
|
||||
410
src/RideFileCommand.cpp
Normal file
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideFile.h"
|
||||
#include "RideFileCommand.h"
|
||||
#include "RideEditor.h"
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
// comparing doubles is nasty
|
||||
static bool doubles_equal(double a, double b)
|
||||
{
|
||||
double errorB = b * DBL_EPSILON;
|
||||
return (a >= b - errorB) && (a <= b + errorB);
|
||||
}
|
||||
//----------------------------------------------------------------------
|
||||
// The public interface to the commands
|
||||
//----------------------------------------------------------------------
|
||||
RideFileCommand::RideFileCommand(RideFile *ride) : ride(ride), stackptr(0), inLUW(false), luw(NULL)
|
||||
{
|
||||
connect(ride, SIGNAL(saved()), this, SLOT(clearHistory()));
|
||||
connect(ride, SIGNAL(reverted()), this, SLOT(clearHistory()));
|
||||
}
|
||||
|
||||
RideFileCommand::~RideFileCommand()
|
||||
{
|
||||
clearHistory();
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::setPointValue(int index, RideFile::SeriesType series, double value)
|
||||
{
|
||||
SetPointValueCommand *cmd = new SetPointValueCommand(ride, index, series,
|
||||
ride->getPointValue(index, series), value);
|
||||
doCommand(cmd);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::deletePoint(int index)
|
||||
{
|
||||
RideFilePoint current = *ride->dataPoints()[index];
|
||||
DeletePointCommand *cmd = new DeletePointCommand(ride, index, current);
|
||||
doCommand(cmd);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::deletePoints(int index, int count)
|
||||
{
|
||||
QVector<RideFilePoint> current;
|
||||
for(int i=0; i< count; i++) {
|
||||
RideFilePoint point = *ride->dataPoints()[index+i];
|
||||
current.append(point);
|
||||
}
|
||||
DeletePointsCommand *cmd = new DeletePointsCommand(ride, index, count, current);
|
||||
doCommand(cmd);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::insertPoint(int index, RideFilePoint *points)
|
||||
{
|
||||
InsertPointCommand *cmd = new InsertPointCommand(ride, index, points);
|
||||
doCommand(cmd);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::appendPoints(QVector <RideFilePoint> newRows)
|
||||
{
|
||||
AppendPointsCommand *cmd = new AppendPointsCommand(ride, ride->dataPoints().count(), newRows);
|
||||
doCommand(cmd);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::setDataPresent(RideFile::SeriesType type, bool newvalue)
|
||||
{
|
||||
bool oldvalue = ride->isDataPresent(type);
|
||||
SetDataPresentCommand *cmd = new SetDataPresentCommand(ride, type, newvalue, oldvalue);
|
||||
doCommand(cmd);
|
||||
}
|
||||
|
||||
QString
|
||||
RideFileCommand::changeLog()
|
||||
{
|
||||
QString log;
|
||||
for (int i=0; i<stackptr; i++) {
|
||||
if (stack[i]->type != RideCommand::NoOp)
|
||||
log += stack[i]->description + '\n';
|
||||
}
|
||||
return log;
|
||||
}
|
||||
//----------------------------------------------------------------------
|
||||
// Manage the Command Stack
|
||||
//----------------------------------------------------------------------
|
||||
void
|
||||
RideFileCommand::clearHistory()
|
||||
{
|
||||
foreach (RideCommand *cmd, stack) delete cmd;
|
||||
stack.clear();
|
||||
stackptr = 0;
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::startLUW(QString name)
|
||||
{
|
||||
luw = new LUWCommand(this, name, ride);
|
||||
inLUW = true;
|
||||
beginCommand(false, luw);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::endLUW()
|
||||
{
|
||||
if (inLUW == false) return; // huh?
|
||||
inLUW = false;
|
||||
|
||||
// add to the stack if it isn't empty
|
||||
if (luw->worklist.count()) doCommand(luw, true);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::doCommand(RideCommand *cmd, bool noexec)
|
||||
{
|
||||
|
||||
// we must add to the LUW, but also must
|
||||
// execute immediately since state data
|
||||
// is collected by each command as it is
|
||||
// created.
|
||||
if (inLUW) {
|
||||
luw->addCommand(cmd);
|
||||
beginCommand(false, cmd);
|
||||
cmd->doCommand(); // luw must be executed as added!!!
|
||||
cmd->docount++;
|
||||
endCommand(false, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// place onto stack
|
||||
if (stack.count()) {
|
||||
stack.remove(stackptr, stack.count() - stackptr);
|
||||
// XXX mem leak need to delete
|
||||
}
|
||||
stack.append(cmd);
|
||||
stackptr++;
|
||||
|
||||
if (noexec == false) {
|
||||
beginCommand(false, cmd); // signal
|
||||
cmd->doCommand(); // execute
|
||||
}
|
||||
cmd->docount++;
|
||||
endCommand(false, cmd); // signal - even if LUW
|
||||
|
||||
// we changed it!
|
||||
ride->emitModified();
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::redoCommand()
|
||||
{
|
||||
if (stackptr < stack.count()) {
|
||||
beginCommand(false, stack[stackptr]); // signal
|
||||
stack[stackptr]->doCommand();
|
||||
stack[stackptr]->docount++;
|
||||
stackptr++; // increment before end to keep in sync in case
|
||||
// it is queried 'after' the command is executed
|
||||
// i.e. within a slot connected to this signal
|
||||
endCommand(false, stack[stackptr-1]); // signal
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::undoCommand()
|
||||
{
|
||||
if (stackptr > 0) {
|
||||
stackptr--;
|
||||
|
||||
beginCommand(true, stack[stackptr]); // signal
|
||||
stack[stackptr]->undoCommand();
|
||||
endCommand(true, stack[stackptr]); // signal
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
RideFileCommand::undoCount()
|
||||
{
|
||||
return stackptr;
|
||||
}
|
||||
|
||||
int
|
||||
RideFileCommand::redoCount()
|
||||
{
|
||||
return stack.count() - stackptr;
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::emitBeginCommand(bool undo, RideCommand *cmd)
|
||||
{
|
||||
emit beginCommand(undo, cmd);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileCommand::emitEndCommand(bool undo, RideCommand *cmd)
|
||||
{
|
||||
emit endCommand(undo, cmd);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Commands...
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// Logical Unit of work
|
||||
LUWCommand::LUWCommand(RideFileCommand *commander, QString name, RideFile *ride) :
|
||||
RideCommand(ride), commander(commander)
|
||||
{
|
||||
type = RideCommand::LUW;
|
||||
description = name;
|
||||
}
|
||||
|
||||
LUWCommand::~LUWCommand()
|
||||
{
|
||||
foreach(RideCommand *cmd, worklist) delete cmd;
|
||||
}
|
||||
|
||||
bool
|
||||
LUWCommand::doCommand()
|
||||
{
|
||||
foreach(RideCommand *cmd, worklist) {
|
||||
commander->emitBeginCommand(false, cmd);
|
||||
cmd->doCommand();
|
||||
commander->emitEndCommand(false, cmd);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
LUWCommand::undoCommand()
|
||||
{
|
||||
for (int i=worklist.count(); i > 0; i--) {
|
||||
RideCommand *cmd = worklist[i-1];
|
||||
commander->emitBeginCommand(true, cmd);
|
||||
cmd->undoCommand();
|
||||
commander->emitEndCommand(true, cmd);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set point value
|
||||
SetPointValueCommand::SetPointValueCommand(RideFile *ride, int row,
|
||||
RideFile::SeriesType series, double oldvalue, double newvalue) :
|
||||
RideCommand(ride), // base class looks after these
|
||||
row(row), series(series), oldvalue(oldvalue), newvalue(newvalue)
|
||||
{
|
||||
type = RideCommand::SetPointValue;
|
||||
description = tr("Set Value");
|
||||
}
|
||||
|
||||
bool
|
||||
SetPointValueCommand::doCommand()
|
||||
{
|
||||
// check it has changed first!
|
||||
if (!doubles_equal(oldvalue, newvalue)) {
|
||||
ride->setPointValue(row,series,newvalue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
SetPointValueCommand::undoCommand()
|
||||
{
|
||||
if (!doubles_equal(oldvalue, newvalue)) {
|
||||
ride->setPointValue(row,series,oldvalue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a point
|
||||
DeletePointCommand::DeletePointCommand(RideFile *ride, int row, RideFilePoint point) :
|
||||
RideCommand(ride), // base class looks after these
|
||||
row(row), point(point)
|
||||
{
|
||||
type = RideCommand::DeletePoint;
|
||||
description = tr("Remove Point");
|
||||
}
|
||||
|
||||
bool
|
||||
DeletePointCommand::doCommand()
|
||||
{
|
||||
ride->deletePoint(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
DeletePointCommand::undoCommand()
|
||||
{
|
||||
ride->insertPoint(row, new RideFilePoint(point));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove points
|
||||
DeletePointsCommand::DeletePointsCommand(RideFile *ride, int row, int count,
|
||||
QVector<RideFilePoint> current) :
|
||||
RideCommand(ride), // base class looks after these
|
||||
row(row), count(count), points(current)
|
||||
{
|
||||
type = RideCommand::DeletePoints;
|
||||
description = tr("Remove Points");
|
||||
}
|
||||
|
||||
bool
|
||||
DeletePointsCommand::doCommand()
|
||||
{
|
||||
ride->deletePoints(row, count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
DeletePointsCommand::undoCommand()
|
||||
{
|
||||
for (int i=(count-1); i>=0; i--) ride->insertPoint(row, new RideFilePoint(points[i]));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Insert a point
|
||||
InsertPointCommand::InsertPointCommand(RideFile *ride, int row, RideFilePoint *point) :
|
||||
RideCommand(ride), // base class looks after these
|
||||
row(row), point(*point)
|
||||
{
|
||||
type = RideCommand::InsertPoint;
|
||||
description = tr("Insert Point");
|
||||
}
|
||||
|
||||
bool
|
||||
InsertPointCommand::doCommand()
|
||||
{
|
||||
ride->insertPoint(row, new RideFilePoint(point));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
InsertPointCommand::undoCommand()
|
||||
{
|
||||
ride->deletePoint(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Append points
|
||||
AppendPointsCommand::AppendPointsCommand(RideFile *ride, int row, QVector<RideFilePoint> points) :
|
||||
RideCommand(ride), // base class looks after these
|
||||
row(row), count (points.count()), points(points)
|
||||
{
|
||||
type = RideCommand::AppendPoints;
|
||||
description = tr("Append Points");
|
||||
}
|
||||
|
||||
bool
|
||||
AppendPointsCommand::doCommand()
|
||||
{
|
||||
QVector<RideFilePoint *> newPoints;
|
||||
foreach (RideFilePoint point, points) {
|
||||
RideFilePoint *p = new RideFilePoint(point);
|
||||
newPoints.append(p);
|
||||
}
|
||||
ride->appendPoints(newPoints);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
AppendPointsCommand::undoCommand()
|
||||
{
|
||||
for (int i=0; i<count; i++) ride->deletePoint(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
SetDataPresentCommand::SetDataPresentCommand(RideFile *ride,
|
||||
RideFile::SeriesType series, bool newvalue, bool oldvalue) :
|
||||
RideCommand(ride), // base class looks after these
|
||||
series(series), oldvalue(oldvalue), newvalue(newvalue)
|
||||
{
|
||||
type = RideCommand::SetDataPresent;
|
||||
description = tr("Set Data Present");
|
||||
}
|
||||
|
||||
bool
|
||||
SetDataPresentCommand::doCommand()
|
||||
{
|
||||
ride->setDataPresent(series, newvalue);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
SetDataPresentCommand::undoCommand()
|
||||
{
|
||||
ride->setDataPresent(series, oldvalue);
|
||||
return true;
|
||||
}
|
||||
194
src/RideFileCommand.h
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _RideFileCommand_h
|
||||
#define _RideFileCommand_h
|
||||
|
||||
#include <QObject>
|
||||
#include <QDate>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVector>
|
||||
|
||||
#include "RideFile.h"
|
||||
|
||||
// for modifying ride data - implements the command pattern
|
||||
// for undo/redo functionality
|
||||
class RideCommand;
|
||||
class LUWCommand;
|
||||
|
||||
class RideFileCommand : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class LUWCommand;
|
||||
|
||||
public:
|
||||
RideFileCommand(RideFile *ride);
|
||||
~RideFileCommand();
|
||||
|
||||
void setPointValue(int index, RideFile::SeriesType series, double value);
|
||||
void deletePoint(int index);
|
||||
void deletePoints(int index, int count);
|
||||
void insertPoint(int index, RideFilePoint *point);
|
||||
void appendPoints(QVector <struct RideFilePoint> newRows);
|
||||
void setDataPresent(RideFile::SeriesType, bool);
|
||||
|
||||
// execute atomic actions
|
||||
void doCommand(RideCommand*, bool noexec=false);
|
||||
void undoCommand();
|
||||
void redoCommand();
|
||||
|
||||
// stack status
|
||||
void startLUW(QString name);
|
||||
void endLUW();
|
||||
|
||||
// change status
|
||||
QString changeLog();
|
||||
int undoCount();
|
||||
int redoCount();
|
||||
|
||||
public slots:
|
||||
void clearHistory();
|
||||
|
||||
signals:
|
||||
void beginCommand(bool undo, RideCommand *cmd);
|
||||
void endCommand(bool undo, RideCommand *cmd);
|
||||
|
||||
protected:
|
||||
void emitBeginCommand(bool, RideCommand *cmd);
|
||||
void emitEndCommand(bool, RideCommand *cmd);
|
||||
|
||||
private:
|
||||
RideFile *ride;
|
||||
QVector<RideCommand *> stack;
|
||||
int stackptr;
|
||||
bool inLUW;
|
||||
LUWCommand *luw;
|
||||
};
|
||||
|
||||
// The Command itself, as a base class with
|
||||
// subclasses for each type
|
||||
class RideCommand
|
||||
{
|
||||
public:
|
||||
// supported command types
|
||||
enum commandtype { NoOp, LUW, SetPointValue, DeletePoint, DeletePoints, InsertPoint, AppendPoints, SetDataPresent };
|
||||
typedef enum commandtype CommandType;
|
||||
|
||||
RideCommand(RideFile *ride) : type(NoOp), ride(ride), docount(0) {}
|
||||
virtual bool doCommand() { return true; }
|
||||
virtual bool undoCommand() { return true; }
|
||||
|
||||
// state of selection model -- if passed at all
|
||||
CommandType type;
|
||||
QString description;
|
||||
RideFile *ride;
|
||||
int docount; // how many times has this been executed?
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// The commands
|
||||
//----------------------------------------------------------------------
|
||||
class LUWCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
LUWCommand(RideFileCommand *command, QString name, RideFile *ride);
|
||||
~LUWCommand(); // needs to clear worklist entries
|
||||
|
||||
void addCommand(RideCommand *cmd) { worklist.append(cmd); }
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
|
||||
QVector<RideCommand*> worklist;
|
||||
RideFileCommand *commander;
|
||||
};
|
||||
|
||||
class SetPointValueCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
SetPointValueCommand(RideFile *ride, int row, RideFile::SeriesType series, double oldvalue, double newvalue);
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
|
||||
// state
|
||||
int row;
|
||||
RideFile::SeriesType series;
|
||||
double oldvalue, newvalue;
|
||||
};
|
||||
|
||||
class DeletePointCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
DeletePointCommand(RideFile *ride, int row, RideFilePoint point);
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
|
||||
// state
|
||||
int row;
|
||||
RideFilePoint point;
|
||||
};
|
||||
|
||||
class DeletePointsCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
DeletePointsCommand(RideFile *ride, int row, int count,
|
||||
QVector<RideFilePoint> current);
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
|
||||
// state
|
||||
int row;
|
||||
int count;
|
||||
QVector<RideFilePoint> points;
|
||||
};
|
||||
|
||||
class InsertPointCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
InsertPointCommand(RideFile *ride, int row, RideFilePoint *point);
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
|
||||
// state
|
||||
int row;
|
||||
RideFilePoint point;
|
||||
};
|
||||
class AppendPointsCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
AppendPointsCommand(RideFile *ride, int row, QVector<RideFilePoint> points);
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
|
||||
int row, count;
|
||||
QVector<RideFilePoint> points;
|
||||
};
|
||||
class SetDataPresentCommand : public RideCommand
|
||||
{
|
||||
public:
|
||||
SetDataPresentCommand(RideFile *ride, RideFile::SeriesType series,
|
||||
bool newvalue, bool oldvalue);
|
||||
bool doCommand();
|
||||
bool undoCommand();
|
||||
RideFile::SeriesType series;
|
||||
bool oldvalue, newvalue;
|
||||
};
|
||||
#endif // _RideFileCommand_h
|
||||
380
src/RideFileTableModel.cpp
Normal file
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideFileTableModel.h"
|
||||
|
||||
RideFileTableModel::RideFileTableModel(RideFile *ride) : ride(ride)
|
||||
{
|
||||
setRide(ride);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setRide(RideFile *newride)
|
||||
{
|
||||
// QPointer helps us check if the current ride has been deleted before trying to disconnect
|
||||
static QPointer<RideFileCommand> connection = NULL;
|
||||
if (connection) {
|
||||
disconnect(connection, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*)));
|
||||
disconnect(connection, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*)));
|
||||
}
|
||||
|
||||
ride = newride;
|
||||
tooltips.clear(); // remove the tooltips -- rideEditor will set them (this is fugly, but efficient)
|
||||
|
||||
if (ride) {
|
||||
|
||||
// set the headings to reflect the data that is present
|
||||
setHeadings();
|
||||
|
||||
// Trap commands
|
||||
connection = ride->command;
|
||||
connect(ride->command, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*)));
|
||||
connect(ride->command, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*)));
|
||||
|
||||
// refresh
|
||||
emit layoutChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setHeadings(RideFile::SeriesType series)
|
||||
{
|
||||
headings_.clear();
|
||||
headingsType.clear();
|
||||
|
||||
// set the headings array
|
||||
if (series == RideFile::secs || ride->areDataPresent()->secs) {
|
||||
headings_ << tr("Time");
|
||||
headingsType << RideFile::secs;
|
||||
}
|
||||
if (series == RideFile::km || ride->areDataPresent()->km) {
|
||||
headings_ << tr("Distance");
|
||||
headingsType << RideFile::km;
|
||||
}
|
||||
if (series == RideFile::watts || ride->areDataPresent()->watts) {
|
||||
headings_ << tr("Power");
|
||||
headingsType << RideFile::watts;
|
||||
}
|
||||
if (series == RideFile::nm || ride->areDataPresent()->nm) {
|
||||
headings_ << tr("Torque");
|
||||
headingsType << RideFile::nm;
|
||||
}
|
||||
if (series == RideFile::cad || ride->areDataPresent()->cad) {
|
||||
headings_ << tr("Cadence");
|
||||
headingsType << RideFile::cad;
|
||||
}
|
||||
if (series == RideFile::hr || ride->areDataPresent()->hr) {
|
||||
headings_ << tr("Heartrate");
|
||||
headingsType << RideFile::hr;
|
||||
}
|
||||
if (series == RideFile::kph || ride->areDataPresent()->kph) {
|
||||
headings_ << tr("Speed");
|
||||
headingsType << RideFile::kph;
|
||||
}
|
||||
if (series == RideFile::alt || ride->areDataPresent()->alt) {
|
||||
headings_ << tr("Altitude");
|
||||
headingsType << RideFile::alt;
|
||||
}
|
||||
if (series == RideFile::lat || ride->areDataPresent()->lat) {
|
||||
headings_ << tr("Latitude");
|
||||
headingsType << RideFile::lat;
|
||||
}
|
||||
if (series == RideFile::lon || ride->areDataPresent()->lon) {
|
||||
headings_ << tr("Longitude");
|
||||
headingsType << RideFile::lon;
|
||||
}
|
||||
if (series == RideFile::headwind || ride->areDataPresent()->headwind) {
|
||||
headings_ << tr("Headwind");
|
||||
headingsType << RideFile::headwind;
|
||||
}
|
||||
if (series == RideFile::interval || ride->areDataPresent()->interval) {
|
||||
headings_ << tr("Interval");
|
||||
headingsType << RideFile::interval;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Qt::ItemFlags
|
||||
RideFileTableModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::ItemIsEditable;
|
||||
else
|
||||
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable |
|
||||
Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
QVariant
|
||||
RideFileTableModel::data(const QModelIndex & index, int role) const
|
||||
{
|
||||
if (role == Qt::ToolTipRole) return toolTip(index.row(), columnType(index.column()));
|
||||
|
||||
if (index.row() >= ride->dataPoints().count() || index.column() >= headings_.count())
|
||||
return QVariant();
|
||||
else
|
||||
return ride->getPointValue(index.row(), headingsType[index.column()]);
|
||||
}
|
||||
|
||||
QVariant
|
||||
RideFileTableModel::headerData(int section, Qt::Orientation orient, int role) const
|
||||
{
|
||||
if (role != Qt::DisplayRole) return QVariant();
|
||||
|
||||
if (orient == Qt::Horizontal) {
|
||||
if (section >= headings_.count())
|
||||
return QVariant();
|
||||
else {
|
||||
return headings_[section];
|
||||
}
|
||||
} else {
|
||||
return QString("%1").arg(section+1);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
RideFileTableModel::rowCount(const QModelIndex &) const
|
||||
{
|
||||
if (ride) return ride->dataPoints().count();
|
||||
else return 0;
|
||||
}
|
||||
|
||||
int
|
||||
RideFileTableModel::columnCount(const QModelIndex &) const
|
||||
{
|
||||
if (ride) return headingsType.count();
|
||||
else return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::setData(const QModelIndex & index, const QVariant &value, int role)
|
||||
{
|
||||
if (index.row() >= ride->dataPoints().count() || index.column() >= headings_.count())
|
||||
return false;
|
||||
else if (role == Qt::EditRole) {
|
||||
ride->command->setPointValue(index.row(), headingsType[index.column()], value.toDouble());
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::setHeaderData(int section, Qt::Orientation , const QVariant & value, int)
|
||||
{
|
||||
if (section >= headings_.count()) return false;
|
||||
else {
|
||||
headings_[section] = value.toString();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertRow(int row, const QModelIndex &parent)
|
||||
{
|
||||
return insertRows(row, 1, parent);
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertRows(int row, int count, const QModelIndex &)
|
||||
{
|
||||
if (row >= ride->dataPoints().count()) return false;
|
||||
else {
|
||||
while (count--) {
|
||||
struct RideFilePoint *p = new RideFilePoint;
|
||||
ride->command->insertPoint(row, p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::appendRows(QVector<RideFilePoint>newRows)
|
||||
{
|
||||
ride->command->appendPoints(newRows);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::removeRows(int row, int count, const QModelIndex &)
|
||||
{
|
||||
if ((row + count) > ride->dataPoints().count()) return false;
|
||||
ride->command->deletePoints(row, count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertColumn(RideFile::SeriesType series)
|
||||
{
|
||||
if (headingsType.contains(series)) return false; // already there
|
||||
|
||||
ride->command->setDataPresent(series, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertColumns(int, int, const QModelIndex &)
|
||||
{
|
||||
// WE DON'T SUPPORT THIS
|
||||
// use insertColumn(RideFile::SeriesType) instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::removeColumn(RideFile::SeriesType series)
|
||||
{
|
||||
if (headingsType.contains(series)) {
|
||||
ride->command->setDataPresent(series, false);
|
||||
return true;
|
||||
} else
|
||||
return false; // its not there
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::removeColumns (int , int , const QModelIndex &)
|
||||
{
|
||||
// WE DON'T SUPPORT THIS
|
||||
// use removeColumn(RideFile::SeriesType) instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setValue(int row, int column, double value)
|
||||
{
|
||||
ride->command->setPointValue(row, headingsType[column], value);
|
||||
}
|
||||
|
||||
double
|
||||
RideFileTableModel::getValue(int row, int column)
|
||||
{
|
||||
return ride->getPointValue(row, headingsType[column]);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::forceRedraw()
|
||||
{
|
||||
// tell the view to redraw everything
|
||||
dataChanged(createIndex(0,0), createIndex(headingsType.count(), ride->dataPoints().count()));
|
||||
}
|
||||
|
||||
//
|
||||
// RideCommand has made changes...
|
||||
//
|
||||
void
|
||||
RideFileTableModel::beginCommand(bool undo, RideCommand *cmd)
|
||||
{
|
||||
switch (cmd->type) {
|
||||
|
||||
case RideCommand::SetPointValue:
|
||||
break;
|
||||
|
||||
case RideCommand::InsertPoint:
|
||||
{
|
||||
InsertPointCommand *dp = (InsertPointCommand *)cmd;
|
||||
if (!undo) beginInsertRows(QModelIndex(), dp->row, dp->row);
|
||||
else beginRemoveRows(QModelIndex(), dp->row, dp->row);
|
||||
break;
|
||||
}
|
||||
|
||||
case RideCommand::DeletePoint:
|
||||
{
|
||||
DeletePointCommand *dp = (DeletePointCommand *)cmd;
|
||||
if (undo) beginInsertRows(QModelIndex(), dp->row, dp->row);
|
||||
else beginRemoveRows(QModelIndex(), dp->row, dp->row);
|
||||
break;
|
||||
}
|
||||
|
||||
case RideCommand::DeletePoints:
|
||||
{
|
||||
DeletePointsCommand *ds = (DeletePointsCommand *)cmd;
|
||||
if (undo) beginInsertRows(QModelIndex(), ds->row, ds->row + ds->count - 1);
|
||||
else beginRemoveRows(QModelIndex(), ds->row, ds->row + ds->count - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case RideCommand::AppendPoints:
|
||||
{
|
||||
AppendPointsCommand *ap = (AppendPointsCommand *)cmd;
|
||||
if (!undo) beginInsertRows(QModelIndex(), ap->row, ap->row + ap->count - 1);
|
||||
else beginRemoveRows(QModelIndex(), ap->row, ap->row + ap->count - 1);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::endCommand(bool undo, RideCommand *cmd)
|
||||
{
|
||||
switch (cmd->type) {
|
||||
|
||||
case RideCommand::SetPointValue:
|
||||
{
|
||||
SetPointValueCommand *spv = (SetPointValueCommand*)cmd;
|
||||
QModelIndex cell(index(spv->row,headingsType.indexOf(spv->series)));
|
||||
dataChanged(cell, cell);
|
||||
break;
|
||||
}
|
||||
case RideCommand::InsertPoint:
|
||||
if (!undo) endInsertRows();
|
||||
else endRemoveRows();
|
||||
break;
|
||||
|
||||
case RideCommand::DeletePoint:
|
||||
case RideCommand::DeletePoints:
|
||||
if (undo) endInsertRows();
|
||||
else endRemoveRows();
|
||||
break;
|
||||
|
||||
case RideCommand::AppendPoints:
|
||||
if (undo) endRemoveRows();
|
||||
else endInsertRows();
|
||||
break;
|
||||
|
||||
case RideCommand::SetDataPresent:
|
||||
setHeadings();
|
||||
emit layoutChanged();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips are kept in a QMap, since they SHOULD be sparse
|
||||
static QString xsstring(int x, RideFile::SeriesType series)
|
||||
{
|
||||
return QString("%1:%2").arg((int)x).arg(static_cast<int>(series));
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setToolTip(int row, RideFile::SeriesType series, QString text)
|
||||
{
|
||||
QString key = xsstring(row, series);
|
||||
|
||||
// if text is blank we are removing it
|
||||
if (text == "") tooltips.remove(key);
|
||||
|
||||
// if text is non-blank we are changing it
|
||||
if (text != "") tooltips.insert(key, text);
|
||||
}
|
||||
|
||||
QString
|
||||
RideFileTableModel::toolTip(int row, RideFile::SeriesType series) const
|
||||
{
|
||||
QString key = xsstring(row, series);
|
||||
return tooltips.value(key, "");
|
||||
}
|
||||
96
src/RideFileTableModel.h
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_RideFileTableModel_h
|
||||
#define _GC_RideFileTableModel_h 1
|
||||
|
||||
#include "RideFile.h"
|
||||
#include "RideFileCommand.h"
|
||||
#include "MainWindow.h"
|
||||
#include <QAbstractTableModel>
|
||||
|
||||
//
|
||||
// Provides a QAbstractTableModel interface to a ridefile and can be used as a
|
||||
// model in a QTableView to view RideFile datapoints. Used by the RideEditor
|
||||
//
|
||||
// All modifications are made via the RideFileCommand interface to ensure the
|
||||
// command pattern is honored for undo/redo etc.
|
||||
//
|
||||
class RideFileTableModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
RideFileTableModel(RideFile *ride = 0);
|
||||
|
||||
// when we choose new ride
|
||||
void setRide(RideFile *ride);
|
||||
|
||||
// used by the view - mandatory implementation
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const;
|
||||
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
int columnCount(const QModelIndex & parent = QModelIndex()) const;
|
||||
bool setData(const QModelIndex & index, const QVariant &value, int role = Qt::EditRole);
|
||||
bool setHeaderData(int section, Qt::Orientation orientation, const QVariant & value, int role = Qt::EditRole);
|
||||
bool insertRow(int row, const QModelIndex & parent = QModelIndex());
|
||||
bool insertRows(int row, int count, const QModelIndex & parent = QModelIndex());
|
||||
bool removeRows(int row, int count, const QModelIndex & parent = QModelIndex());
|
||||
bool appendRows(QVector<RideFilePoint>newRows);
|
||||
|
||||
// DO NOT USE THESE -- THEY ARE NULL --
|
||||
bool insertColumns (int column, int count, const QModelIndex & parent = QModelIndex());
|
||||
bool removeColumns (int column, int count, const QModelIndex & parent = QModelIndex());
|
||||
// USE THESE INSTEAD
|
||||
bool insertColumn(RideFile::SeriesType series);
|
||||
bool removeColumn(RideFile::SeriesType series);
|
||||
|
||||
// get/set value by column number
|
||||
void setValue(int row, int column, double value);
|
||||
double getValue(int row, int column);
|
||||
|
||||
// get/interpret headings
|
||||
QStringList &headings() { return headings_; }
|
||||
RideFile::SeriesType columnType(int index) const { return headingsType[index]; }
|
||||
int columnFor(RideFile::SeriesType series) const { return headingsType.indexOf(series); }
|
||||
|
||||
// get/set tooltip
|
||||
void setToolTip(int row, RideFile::SeriesType series, QString value);
|
||||
QString toolTip(int row, RideFile::SeriesType series) const;
|
||||
|
||||
public slots:
|
||||
// RideCommand signals trapped here
|
||||
void beginCommand(bool undo, RideCommand *);
|
||||
void endCommand(bool undo, RideCommand *);
|
||||
|
||||
// force redraw - used by anomaly detection and find
|
||||
void forceRedraw();
|
||||
|
||||
protected:
|
||||
|
||||
private:
|
||||
RideFile *ride;
|
||||
QMap <QString,QString> tooltips;
|
||||
|
||||
QStringList headings_;
|
||||
QVector<RideFile::SeriesType> headingsType;
|
||||
void setHeadings(RideFile::SeriesType series = RideFile::none);
|
||||
};
|
||||
#endif // _GC_RideFileTableModel_h
|
||||
@@ -16,6 +16,7 @@
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include <QTreeWidgetItem>
|
||||
#include "RideItem.h"
|
||||
#include "RideMetric.h"
|
||||
#include "RideFile.h"
|
||||
@@ -27,7 +28,7 @@
|
||||
RideItem::RideItem(int type,
|
||||
QString path, QString fileName, const QDateTime &dateTime,
|
||||
const Zones *zones, QString notesFileName, MainWindow *main) :
|
||||
QTreeWidgetItem(type), ride_(NULL), main(main), isdirty(false), path(path), fileName(fileName),
|
||||
QTreeWidgetItem(type), ride_(NULL), main(main), isdirty(false), isedit(false), path(path), fileName(fileName),
|
||||
dateTime(dateTime), zones(zones), notesFileName(notesFileName)
|
||||
{
|
||||
setText(0, dateTime.toString("ddd"));
|
||||
@@ -44,11 +45,43 @@ RideFile *RideItem::ride()
|
||||
// open the ride file
|
||||
QFile file(path + "/" + fileName);
|
||||
ride_ = RideFileFactory::instance().openRideFile(file, errors_);
|
||||
setDirty(false); // we're gonna use on-disk so by
|
||||
// definition it is clean - but do it *after*
|
||||
// we read the file since it will almost
|
||||
// certainly be referenced by consuming widgets
|
||||
|
||||
// stay aware of state changes to our ride
|
||||
// MainWindow saves and RideFileCommand modifies
|
||||
connect(ride_, SIGNAL(modified()), this, SLOT(modified()));
|
||||
connect(ride_, SIGNAL(saved()), this, SLOT(saved()));
|
||||
connect(ride_, SIGNAL(reverted()), this, SLOT(reverted()));
|
||||
|
||||
return ride_;
|
||||
}
|
||||
|
||||
void
|
||||
RideItem::modified()
|
||||
{
|
||||
setDirty(true);
|
||||
}
|
||||
|
||||
void
|
||||
RideItem::saved()
|
||||
{
|
||||
setDirty(false);
|
||||
}
|
||||
|
||||
void
|
||||
RideItem::reverted()
|
||||
{
|
||||
setDirty(false);
|
||||
}
|
||||
|
||||
void
|
||||
RideItem::setDirty(bool val)
|
||||
{
|
||||
if (isdirty == val) return; // np change
|
||||
|
||||
isdirty = val;
|
||||
|
||||
if (isdirty == true) {
|
||||
@@ -60,6 +93,8 @@ RideItem::setDirty(bool val)
|
||||
setFont(i, current);
|
||||
}
|
||||
|
||||
main->notifyRideDirty();
|
||||
|
||||
} else {
|
||||
|
||||
// show ride in normal on the list view
|
||||
@@ -68,6 +103,8 @@ RideItem::setDirty(bool val)
|
||||
current.setWeight(QFont::Normal);
|
||||
setFont(i, current);
|
||||
}
|
||||
|
||||
main->notifyRideClean();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,24 @@
|
||||
#define _GC_RideItem_h 1
|
||||
|
||||
#include <QtGui>
|
||||
#include <QTreeWidgetItem>
|
||||
#include "RideMetric.h"
|
||||
|
||||
class RideFile;
|
||||
class RideEditor;
|
||||
class MainWindow;
|
||||
class Zones;
|
||||
|
||||
class RideItem : public QTreeWidgetItem {
|
||||
// Because we have subclassed QTreeWidgetItem we
|
||||
// need to use our own type, this MUST be greater than
|
||||
// QTreeWidgetItem::UserType according to the docs
|
||||
#define FOLDER_TYPE 0
|
||||
#define RIDE_TYPE (QTreeWidgetItem::UserType+1)
|
||||
|
||||
class RideItem : public QObject, public QTreeWidgetItem //<< for signals/slots
|
||||
{
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
|
||||
@@ -36,12 +47,19 @@ class RideItem : public QTreeWidgetItem {
|
||||
MainWindow *main; // to notify widgets when date/time changes
|
||||
bool isdirty;
|
||||
|
||||
public slots:
|
||||
void modified();
|
||||
void reverted();
|
||||
void saved();
|
||||
|
||||
public:
|
||||
|
||||
bool isedit; // is being edited at the moment
|
||||
|
||||
QString path;
|
||||
QString fileName;
|
||||
QDateTime dateTime;
|
||||
QDateTime computeMetricsTime;
|
||||
QDateTime computeMetricsTime;
|
||||
RideFile *ride();
|
||||
const QStringList errors() { return errors_; }
|
||||
const Zones *zones;
|
||||
@@ -65,4 +83,3 @@ class RideItem : public QTreeWidgetItem {
|
||||
double timeInZone(int zone);
|
||||
};
|
||||
#endif // _GC_RideItem_h
|
||||
|
||||
|
||||
@@ -221,7 +221,13 @@ FormField::FormField(FieldDefinition field, MainWindow *main) : definition(field
|
||||
widget = main->rideNotesWidget();
|
||||
} else {
|
||||
widget = new QTextEdit(this);
|
||||
connect (widget, SIGNAL(textChanged()), this, SLOT(editFinished()));
|
||||
if (field.name == "Change History") {
|
||||
dynamic_cast<QTextEdit*>(widget)->setReadOnly(true);
|
||||
// pick up when ride saved - since it gets updated then
|
||||
connect (main, SIGNAL(rideClean()), this, SLOT(rideSelected()));
|
||||
} else {
|
||||
connect (widget, SIGNAL(textChanged()), this, SLOT(editFinished()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "GcRideFile.h"
|
||||
#include "RideItem.h"
|
||||
#include "RideFile.h"
|
||||
#include "RideFileCommand.h"
|
||||
#include "Settings.h"
|
||||
#include "SaveDialogs.h"
|
||||
|
||||
@@ -175,6 +176,13 @@ MainWindow::saveSilent(RideItem *rideItem)
|
||||
savedFile.setFileName(currentFile.fileName());
|
||||
}
|
||||
|
||||
// update the change history
|
||||
QString log = rideItem->ride()->getTag("Change History", "");
|
||||
log += tr("Changes on ");
|
||||
log += QDateTime::currentDateTime().toString() + ":";
|
||||
log += '\n' + rideItem->ride()->command->changeLog();
|
||||
rideItem->ride()->setTag("Change History", log);
|
||||
|
||||
// save in GC format
|
||||
GcFileReader reader;
|
||||
reader.writeRideFile(rideItem->ride(), savedFile);
|
||||
@@ -190,7 +198,7 @@ MainWindow::saveSilent(RideItem *rideItem)
|
||||
}
|
||||
|
||||
// mark clean as we have now saved the data
|
||||
rideItem->setDirty(false);
|
||||
rideItem->ride()->emitSaved();
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
@@ -73,6 +73,13 @@
|
||||
#define GC_DEV_DEFI "devicedefi"
|
||||
#define GC_DEV_DEFR "devicedefr"
|
||||
|
||||
// data processor config
|
||||
#define GC_DPFG_TOLERANCE "dataprocess/fixgaps/tolerance"
|
||||
#define GC_DPFG_STOP "dataprocess/fixgaps/stop"
|
||||
#define GC_DPFS_MAX "dataprocess/fixspikes/max"
|
||||
#define GC_DPFS_VARIANCE "dataprocess/fixspikes/variance"
|
||||
#define GC_DPTA "dataprocess/torqueadjust/adjustment"
|
||||
|
||||
#include <QSettings>
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
|
||||
@@ -34,6 +34,13 @@ SpecialFields::SpecialFields()
|
||||
<< "Weight" // in WKO and possibly others
|
||||
<< "Device" // RideFile::devicetype
|
||||
<< "Device Info" // in WKO and TCX and possibly others
|
||||
<< "Dropouts" // calculated from source data by FixGaps
|
||||
<< "Dropout Time" // calculated from source data vy FixGaps
|
||||
<< "Spikes" // calculated from source data by FixSpikes
|
||||
<< "Spike Time" // calculated from source data by FixSpikes
|
||||
<< "Torque Adjust" // the last torque adjust applied
|
||||
<< "Filename" // set by the rideFile reader
|
||||
<< "Change History" // set by RideFileCommand
|
||||
;
|
||||
|
||||
// now add all the metric fields (for metric overrides)
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
#include <qwt_plot_grid.h>
|
||||
#include <assert.h>
|
||||
|
||||
// XXX: these are also defined in MainWindow.cpp. Fugly.
|
||||
#define FOLDER_TYPE 0
|
||||
#define RIDE_TYPE 1
|
||||
|
||||
WeeklySummaryWindow::WeeklySummaryWindow(bool useMetricUnits,
|
||||
MainWindow *mainWindow) :
|
||||
QWidget(mainWindow), mainWindow(mainWindow),
|
||||
|
||||
@@ -12,5 +12,14 @@
|
||||
<file>translations/gc_ja.qm</file>
|
||||
<file>xml/charts.xml</file>
|
||||
<file>xml/metadata.xml</file>
|
||||
<file>images/toolbar/close-icon.png</file>
|
||||
<file>images/toolbar/save.png</file>
|
||||
<file>images/toolbar/search.png</file>
|
||||
<file>images/toolbar/splash green.png</file>
|
||||
<file>images/toolbar/cut.png</file>
|
||||
<file>images/toolbar/copy.png</file>
|
||||
<file>images/toolbar/paste.png</file>
|
||||
<file>images/toolbar/undo.png</file>
|
||||
<file>images/toolbar/redo.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
BIN
src/images/toolbar/close-icon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/images/toolbar/copy.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/images/toolbar/cut.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/images/toolbar/paste.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/images/toolbar/redo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/images/toolbar/refresh.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/images/toolbar/save.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/images/toolbar/search.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/images/toolbar/splash green.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/images/toolbar/star.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/images/toolbar/undo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
14
src/src.pro
@@ -77,6 +77,7 @@ HEADERS += \
|
||||
DBAccess.h \
|
||||
DatePickerDialog.h \
|
||||
DaysScaleDraw.h \
|
||||
DataProcessor.h \
|
||||
Device.h \
|
||||
DeviceTypes.h \
|
||||
DeviceConfiguration.h \
|
||||
@@ -92,6 +93,7 @@ HEADERS += \
|
||||
LogTimeScaleEngine.h \
|
||||
LTMCanvasPicker.h \
|
||||
LTMChartParser.h \
|
||||
LTMOutliers.h \
|
||||
LTMPlot.h \
|
||||
LTMSettings.h \
|
||||
LTMTool.h \
|
||||
@@ -120,7 +122,10 @@ HEADERS += \
|
||||
ComputrainerController.h \
|
||||
RealtimePlot.h \
|
||||
RideCalendar.h \
|
||||
RideEditor.h \
|
||||
RideFile.h \
|
||||
RideFileCommand.h \
|
||||
RideFileTableModel.h \
|
||||
RideImportWizard.h \
|
||||
RideItem.h \
|
||||
RideMetadata.h \
|
||||
@@ -174,6 +179,7 @@ SOURCES += \
|
||||
CriticalPowerWindow.cpp \
|
||||
CsvRideFile.cpp \
|
||||
DBAccess.cpp \
|
||||
DataProcessor.cpp \
|
||||
DatePickerDialog.cpp \
|
||||
Device.cpp \
|
||||
DeviceTypes.cpp \
|
||||
@@ -182,6 +188,10 @@ SOURCES += \
|
||||
ErgFile.cpp \
|
||||
ErgFilePlot.cpp \
|
||||
FitRideFile.cpp \
|
||||
FixGaps.cpp \
|
||||
FixGPS.cpp \
|
||||
FixSpikes.cpp \
|
||||
FixTorque.cpp \
|
||||
GcRideFile.cpp \
|
||||
GoogleMapControl.cpp \
|
||||
HistogramWindow.cpp \
|
||||
@@ -190,6 +200,7 @@ SOURCES += \
|
||||
LogTimeScaleEngine.cpp \
|
||||
LTMCanvasPicker.cpp \
|
||||
LTMChartParser.cpp \
|
||||
LTMOutliers.cpp \
|
||||
LTMPlot.cpp \
|
||||
LTMSettings.cpp \
|
||||
LTMTool.cpp \
|
||||
@@ -219,7 +230,10 @@ SOURCES += \
|
||||
RealtimeWindow.cpp \
|
||||
RealtimePlot.cpp \
|
||||
RideCalendar.cpp \
|
||||
RideEditor.cpp \
|
||||
RideFile.cpp \
|
||||
RideFileCommand.cpp \
|
||||
RideFileTableModel.cpp \
|
||||
RideImportWizard.cpp \
|
||||
RideItem.cpp \
|
||||
RideMetadata.cpp \
|
||||
|
||||