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.
This commit is contained in:
Mark Liversedge
2010-07-17 14:33:39 +01:00
parent 8569f812a6
commit cd3bbc4e64
41 changed files with 5314 additions and 54 deletions

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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
//

View File

@@ -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

File diff suppressed because it is too large Load Diff

271
src/RideEditor.h Normal file
View 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

View File

@@ -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();
}

View File

@@ -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
View 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
View 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
View 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
View 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

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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();
}
//----------------------------------------------------------------------

View File

@@ -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>

View File

@@ -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)

View File

@@ -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),

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/images/toolbar/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/images/toolbar/cut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/images/toolbar/redo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
src/images/toolbar/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
src/images/toolbar/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
src/images/toolbar/undo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -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 \