mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-15 08:59:55 +00:00
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:
248
src/FixGaps.cpp
Normal file
248
src/FixGaps.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "DataProcessor.h"
|
||||
#include "Settings.h"
|
||||
#include "Units.h"
|
||||
#include <algorithm>
|
||||
#include <QVector>
|
||||
|
||||
#define tr(s) QObject::tr(s)
|
||||
|
||||
// Config widget used by the Preferences/Options config panes
|
||||
class FixGaps;
|
||||
class FixGapsConfig : public DataProcessorConfig
|
||||
{
|
||||
friend class ::FixGaps;
|
||||
protected:
|
||||
QHBoxLayout *layout;
|
||||
QLabel *toleranceLabel, *beerandburritoLabel;
|
||||
QDoubleSpinBox *tolerance,
|
||||
*beerandburrito;
|
||||
|
||||
public:
|
||||
FixGapsConfig(QWidget *parent) : DataProcessorConfig(parent) {
|
||||
|
||||
layout = new QHBoxLayout(this);
|
||||
|
||||
layout->setContentsMargins(0,0,0,0);
|
||||
setContentsMargins(0,0,0,0);
|
||||
|
||||
toleranceLabel = new QLabel(tr("Tolerance"));
|
||||
beerandburritoLabel = new QLabel(tr("Stop"));
|
||||
|
||||
tolerance = new QDoubleSpinBox();
|
||||
tolerance->setMaximum(99.99);
|
||||
tolerance->setMinimum(0);
|
||||
tolerance->setSingleStep(0.1);
|
||||
|
||||
beerandburrito = new QDoubleSpinBox();
|
||||
beerandburrito->setMaximum(99999.99);
|
||||
beerandburrito->setMinimum(0);
|
||||
beerandburrito->setSingleStep(0.1);
|
||||
|
||||
layout->addWidget(toleranceLabel);
|
||||
layout->addWidget(tolerance);
|
||||
layout->addWidget(beerandburritoLabel);
|
||||
layout->addWidget(beerandburrito);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
//~FixGapsConfig() {} // deliberately not declared since Qt will delete
|
||||
// the widget and its children when the config pane is deleted
|
||||
|
||||
QString explain() {
|
||||
return(QString(tr("Many devices, especially wireless devices, will "
|
||||
"drop connections to the bike computer. This leads "
|
||||
"to lost samples in the resulting data, or so-called "
|
||||
"drops in recording.\n\n"
|
||||
"In order to calculate peak powers and averages, it "
|
||||
"is very helpful to remove these gaps, and either "
|
||||
"smooth the data where it is missing or just "
|
||||
"replace with zero value samples\n\n"
|
||||
"This function performs this task, taking two "
|
||||
"parameters;\n\n"
|
||||
"tolerance - this defines the minimum size of a "
|
||||
"recording gap (in seconds) that will be processed. "
|
||||
"any gap shorter than this will not be affected.\n\n"
|
||||
"stop - this defines the maximum size of "
|
||||
"gap (in seconds) that will have a smoothing algorithm "
|
||||
"applied. Where a gap is shorter than this value it will "
|
||||
"be filled with values interpolated from the values "
|
||||
"recorded before and after the gap. If it is longer "
|
||||
"than this value, it will be filled with zero values.")));
|
||||
}
|
||||
|
||||
void readConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
double tol = settings->value(GC_DPFG_TOLERANCE, "1.0").toDouble();
|
||||
double stop = settings->value(GC_DPFG_STOP, "1.0").toDouble();
|
||||
tolerance->setValue(tol);
|
||||
beerandburrito->setValue(stop);
|
||||
}
|
||||
|
||||
void saveConfig() {
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
settings->setValue(GC_DPFG_TOLERANCE, tolerance->value());
|
||||
settings->setValue(GC_DPFG_STOP, beerandburrito->value());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// RideFile Dataprocessor -- used to handle gaps in recording
|
||||
// by inserting interpolated/zero samples
|
||||
// to ensure dataPoints are contiguous in time
|
||||
//
|
||||
class FixGaps : public DataProcessor {
|
||||
|
||||
public:
|
||||
FixGaps() {}
|
||||
~FixGaps() {}
|
||||
|
||||
// the processor
|
||||
bool postProcess(RideFile *, DataProcessorConfig* config);
|
||||
|
||||
// the config widget
|
||||
DataProcessorConfig* processorConfig(QWidget *parent) {
|
||||
return new FixGapsConfig(parent);
|
||||
}
|
||||
};
|
||||
|
||||
static bool fixGapsAdded = DataProcessorFactory::instance().registerProcessor(QString(tr("Fix Gaps in Recording")), new FixGaps());
|
||||
|
||||
bool
|
||||
FixGaps::postProcess(RideFile *ride, DataProcessorConfig *config=0)
|
||||
{
|
||||
// get settings
|
||||
double tolerance, stop;
|
||||
if (config == NULL) { // being called automatically
|
||||
boost::shared_ptr<QSettings> settings = GetApplicationSettings();
|
||||
tolerance = settings->value(GC_DPFG_TOLERANCE, "1.0").toDouble();
|
||||
stop = settings->value(GC_DPFG_STOP, "1.0").toDouble();
|
||||
} else { // being called manually
|
||||
tolerance = ((FixGapsConfig*)(config))->tolerance->value();
|
||||
stop = ((FixGapsConfig*)(config))->beerandburrito->value();
|
||||
}
|
||||
|
||||
// if the number of duration / number of samples
|
||||
// equals the recording interval then we don't need
|
||||
// to post-process for gaps
|
||||
// XXX commented out since it is not always true and
|
||||
// is purely to improve performance
|
||||
//if ((ride->recIntSecs() + ride->dataPoints()[ride->dataPoints().count()-1]->secs -
|
||||
// ride->dataPoints()[0]->secs) / (double) ride->dataPoints().count() == ride->recIntSecs())
|
||||
// return false;
|
||||
|
||||
// Additionally, If there are less than 2 dataPoints then there
|
||||
// is no way of post processing anyway (e.g. manual workouts)
|
||||
if (ride->dataPoints().count() < 2) return false;
|
||||
|
||||
// OK, so there are probably some gaps, lets post process them
|
||||
RideFilePoint *last = NULL;
|
||||
int dropouts = 0;
|
||||
double dropouttime = 0.0;
|
||||
|
||||
// put it all in a LUW
|
||||
ride->command->startLUW("Fix Gaps in Recording");
|
||||
|
||||
for (int position = 0; position < ride->dataPoints().count(); position++) {
|
||||
RideFilePoint *point = ride->dataPoints()[position];
|
||||
|
||||
if (last == NULL) last = point;
|
||||
else {
|
||||
double gap = point->secs - last->secs - ride->recIntSecs();
|
||||
|
||||
// if we have gps and we moved, then this isn't a stop
|
||||
bool stationary = ((last->lat || last->lon) && (point->lat || point->lon)) // gps is present
|
||||
&& last->lat == point->lat && last->lon == point->lon;
|
||||
|
||||
// moved for less than 30 seconds ... interpolate
|
||||
if (!stationary && gap > tolerance && gap < stop) {
|
||||
|
||||
// what's needed?
|
||||
dropouts++;
|
||||
dropouttime += gap;
|
||||
|
||||
int count = gap/ride->recIntSecs();
|
||||
double hrdelta = (point->hr - last->hr) / (double) count;
|
||||
double pwrdelta = (point->watts - last->watts) / (double) count;
|
||||
double kphdelta = (point->kph - last->kph) / (double) count;
|
||||
double kmdelta = (point->km - last->km) / (double) count;
|
||||
double caddelta = (point->cad - last->cad) / (double) count;
|
||||
double altdelta = (point->alt - last->alt) / (double) count;
|
||||
double nmdelta = (point->nm - last->nm) / (double) count;
|
||||
double londelta = (point->lon - last->lon) / (double) count;
|
||||
double latdelta = (point->lat - last->lat) / (double) count;
|
||||
double hwdelta = (point->headwind - last->headwind) / (double) count;
|
||||
|
||||
// add the points
|
||||
for(int i=0; i<count; i++) {
|
||||
RideFilePoint *add = new RideFilePoint(last->secs+((i+1)*ride->recIntSecs()),
|
||||
last->cad+((i+1)*caddelta),
|
||||
last->hr + ((i+1)*hrdelta),
|
||||
last->km + ((i+1)*kmdelta),
|
||||
last->kph + ((i+1)*kphdelta),
|
||||
last->nm + ((i+1)*nmdelta),
|
||||
last->watts + ((i+1)*pwrdelta),
|
||||
last->alt + ((i+1)*altdelta),
|
||||
last->lon + ((i+1)*londelta),
|
||||
last->lat + ((i+1)*latdelta),
|
||||
last->headwind + ((i+1)*hwdelta),
|
||||
last->interval);
|
||||
ride->command->insertPoint(position++, add);
|
||||
}
|
||||
|
||||
// stationary or greater than 30 seconds... fill with zeroes
|
||||
} else if (gap > stop) {
|
||||
|
||||
dropouts++;
|
||||
dropouttime += gap;
|
||||
|
||||
int count = gap/ride->recIntSecs();
|
||||
double kmdelta = (point->km - last->km) / (double) count;
|
||||
|
||||
// add zero value points
|
||||
for(int i=0; i<count; i++) {
|
||||
RideFilePoint *add = new RideFilePoint(last->secs+((i+1)*ride->recIntSecs()),
|
||||
0,
|
||||
0,
|
||||
last->km + ((i+1)*kmdelta),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
last->interval);
|
||||
ride->command->insertPoint(position++, add);
|
||||
}
|
||||
}
|
||||
}
|
||||
last = point;
|
||||
}
|
||||
|
||||
// end the Logical unit of work here
|
||||
ride->command->endLUW();
|
||||
|
||||
ride->setTag("Dropouts", QString("%1").arg(dropouts));
|
||||
ride->setTag("Dropout Time", QString("%1").arg(dropouttime));
|
||||
|
||||
if (dropouts) return true;
|
||||
else return false;
|
||||
}
|
||||
Reference in New Issue
Block a user