diff --git a/src/AllPlotWindow.cpp b/src/AllPlotWindow.cpp index 3ea37d0bb..df063a6bd 100644 --- a/src/AllPlotWindow.cpp +++ b/src/AllPlotWindow.cpp @@ -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; diff --git a/src/DataProcessor.cpp b/src/DataProcessor.cpp new file mode 100644 index 000000000..30c2660dc --- /dev/null +++ b/src/DataProcessor.cpp @@ -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 + +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 settings = GetApplicationSettings(); + bool changed = false; + + // run through the processors and execute them! + QMapIterator 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 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(); +} diff --git a/src/DataProcessor.h b/src/DataProcessor.h new file mode 100644 index 000000000..ead80461c --- /dev/null +++ b/src/DataProcessor.h @@ -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 +#include +#include +#include +#include +#include + +// 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 + +// 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 processors; + DataProcessorFactory() {} + + public: + + static DataProcessorFactory &instance(); + + bool registerProcessor(QString name, DataProcessor *processor); + QMap 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 diff --git a/src/FixGPS.cpp b/src/FixGPS.cpp new file mode 100644 index 000000000..930035fff --- /dev/null +++ b/src/FixGPS.cpp @@ -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 +#include + +#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; idataPoints().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; jcommand->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; jcommand->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; jdataPoints().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; +} diff --git a/src/FixGaps.cpp b/src/FixGaps.cpp new file mode 100644 index 000000000..06e799f42 --- /dev/null +++ b/src/FixGaps.cpp @@ -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 +#include + +#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 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 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 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; isecs+((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; isecs+((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; +} diff --git a/src/FixSpikes.cpp b/src/FixSpikes.cpp new file mode 100644 index 000000000..bc166a40f --- /dev/null +++ b/src/FixSpikes.cpp @@ -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 +#include + +#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 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 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 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 power; + QVector 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; igetDeviationForRank(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; +} diff --git a/src/FixTorque.cpp b/src/FixTorque.cpp new file mode 100644 index 000000000..bb8433835 --- /dev/null +++ b/src/FixTorque.cpp @@ -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 +#include + +#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 settings = GetApplicationSettings(); + ta->setText(settings->value(GC_DPTA, "0 nm").toString()); + } + + void saveConfig() { + boost::shared_ptr 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 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; idataPoints().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; +} diff --git a/src/LTMOutliers.cpp b/src/LTMOutliers.cpp new file mode 100644 index 000000000..d5592c063 --- /dev/null +++ b/src/LTMOutliers.cpp @@ -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 +#include +#include +#include "LTMOutliers.h" + +#include + + +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 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); +} diff --git a/src/LTMOutliers.h b/src/LTMOutliers.h new file mode 100644 index 000000000..735b008d5 --- /dev/null +++ b/src/LTMOutliers.h @@ -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 +#include + +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 rank; // ranked list of x sorted by deviation +}; + +#endif diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 522531a75..905ebabf4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -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 processors = factory.getProcessors(); + + if (processors.count()) { + + optionsMenu->addSeparator(); + toolMapper = new QSignalMapper(this); // maps each option + QMapIterator 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(item); + RideItem *other = static_cast(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(_item); + RideItem *item = static_cast(_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(_item); + RideItem *item = static_cast(_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(); + } +} diff --git a/src/MainWindow.h b/src/MainWindow.h index ee7470082..06bc9b079 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -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 diff --git a/src/Pages.cpp b/src/Pages.cpp index 424899cc6..51b33b8eb 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -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 &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 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 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 settings = GetApplicationSettings(); + for (int i=0; iinvisibleRootItem()->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 // diff --git a/src/Pages.h b/src/Pages.h index b8e45e0fe..2c9909c4c 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -21,6 +21,7 @@ #include "DeviceTypes.h" #include "DeviceConfiguration.h" #include "RideMetadata.h" +#include "DataProcessor.h" class QGroupBox; class QHBoxLayout; @@ -265,6 +266,30 @@ class FieldsPage : public QWidget QPushButton *upButton, *downButton, *addButton, *renameButton, *deleteButton; }; +class ProcessorPage : public QWidget +{ + Q_OBJECT + + public: + + ProcessorPage(MainWindow *main); + void saveClicked(); + + public slots: + + //void upClicked(); + //void downClicked(); + + protected: + + MainWindow *main; + QMap 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 keywordDefinitions; diff --git a/src/RideEditor.cpp b/src/RideEditor.cpp new file mode 100644 index 000000000..85167f709 --- /dev/null +++ b/src/RideEditor.cpp @@ -0,0 +1,2241 @@ +/* + * 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 "RideEditor.h" +#include "LTMOutliers.h" +#include "MainWindow.h" +#include "Settings.h" + +#include +#include + +// used to make a lookup string for row/col anomalies +static QString xsstring(int x, RideFile::SeriesType series) +{ + return QString("%1:%2").arg((int)x).arg(static_cast(series)); +} +static void unxsstring(QString val, int &x, RideFile::SeriesType &series) +{ + QRegExp it("^([^:]*):([^:]*)$"); + it.exactMatch(val); + x = it.cap(1).toDouble(); + series = static_cast(it.cap(2).toInt()); +} + +static void secsMsecs(double value, int &secs, int &msecs) +{ + // split into secs and msecs from a double + // tried modf, floor, round and a host of others but + // they all had difference problems. In the end + // I've resorted to rounding to 100ths of a second. + // I acknowledge that this is horrid, but its ok + // for Powertaps but maybe more precise devices will + // come along? + secs = floor(value); // assume it is positive!! .. it is a time field! + msecs = round((value - secs) * 100) * 10; +} + +RideEditor::RideEditor(MainWindow *main) : QWidget(main), data(NULL), ride(NULL), main(main), inLUW(false) +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + //Left in the code to display a title, but + //its a waste of screen estate, maybe uncomment + //it if someone finds it useful + + //title = new QLabel(tr("No ride selected")); + //QFont font; + //font.setWeight(Qt::black); + //title->setFont(font); + //title->setAlignment(Qt::AlignHCenter); + + // setup the toolbar + toolbar = new QToolBar(this); + toolbar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + toolbar->setFloatable(true); + + QIcon saveIcon(":images/toolbar/save.png"); + saveAct = new QAction(saveIcon, tr("Save"), this); + connect(saveAct, SIGNAL(triggered()), this, SLOT(saveFile())); + toolbar->addAction(saveAct); + + QIcon findIcon(":images/toolbar/search.png"); + searchAct = new QAction(findIcon, tr("Find"), this); + connect(searchAct, SIGNAL(triggered()), this, SLOT(find())); + toolbar->addAction(searchAct); + + // ***************************************************** + // REMOVED MANUALLY RUNNING A CHECK SINCE IT IS NOW + // PRETTY EFFICIENT AND UPDATES AUTOMATICALLY WHEN + // A COMMAND COMPLETES. IF THIS BECOMES TOO MUCH OF A + // PERFORMANCE OVERHEAD THEN WE CAN RE-ADD THIS + // ***************************************************** + //QIcon checkIcon(":images/toolbar/splash green.png"); + //checkAct = new QAction(checkIcon, tr("Check"), this); + //connect(checkAct, SIGNAL(triggered()), this, SLOT(check())); + //toolbar->addAction(checkAct); + + // undo and redo deliberately at a distance from the + // save icon, since accidentally hitting the wrong + // icon in that instance would be horrible + QIcon undoIcon(":images/toolbar/undo.png"); + undoAct = new QAction(undoIcon, tr("Undo"), this); + connect(undoAct, SIGNAL(triggered()), this, SLOT(undo())); + toolbar->addAction(undoAct); + + QIcon redoIcon(":images/toolbar/redo.png"); + redoAct = new QAction(redoIcon, tr("Redo"), this); + connect(redoAct, SIGNAL(triggered()), this, SLOT(redo())); + toolbar->addAction(redoAct); + + // empty model + model = new RideFileTableModel(NULL); + + // set up the table + table = new QTableView(); + table->setItemDelegate(new CellDelegate(this)); + table->verticalHeader()->setDefaultSectionSize(20); + table->setModel(model); + table->setContextMenuPolicy(Qt::CustomContextMenu); + table->setSelectionMode(QAbstractItemView::ContiguousSelection); + table->installEventFilter(this); + + // prettify (and make anomalies more visible) + QPen gridStyle; + gridStyle.setColor(Qt::lightGray); + table->setGridStyle(Qt::DotLine); + + connect(table, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(cellMenu(const QPoint&))); + + // layout the widget + //mainLayout->addWidget(title); + mainLayout->addWidget(toolbar); + mainLayout->addWidget(table); + + // get latest config + configChanged(); + + // trap GC signals + connect(main, SIGNAL(configChanged()), this, SLOT(configChanged())); + connect(main, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); + connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected())); + connect(main, SIGNAL(rideDirty()), this, SLOT(rideDirty())); + connect(main, SIGNAL(rideClean()), this, SLOT(rideClean())); +} + +void +RideEditor::configChanged() +{ + boost::shared_ptr settings = GetApplicationSettings(); + + // get spike config + DPFSmax = settings->value(GC_DPFS_MAX, "1500").toDouble(); + DPFSvariance = settings->value(GC_DPFS_VARIANCE, "1000").toDouble(); +} + + +//---------------------------------------------------------------------- +// RideEditor table/model/rideFile utility functions +//---------------------------------------------------------------------- + + +// what are the available columns? (used by insert column context menu) +QList +RideEditor::whatColumns() +{ + QList what; + + what << tr("Time") + << tr("Distance") + << tr("Power") + << tr("Heartrate") + << tr("Cadence") + << tr("Speed") + << tr("Torque") + << tr("Altitude") + << tr("Latitude") + << tr("Longitude") + << tr("Headwind") + << tr("Interval"); + + return what; +} + +double +RideEditor::getValue(int row, int column) +{ + if (row < 0 || column < 0) return 0.0; + + return ride->ride()->getPointValue(row, model->columnType(column)); +} + +void +RideEditor::setModelValue(int row, int col, double value) +{ + model->setValue(row, col, value); +} + +bool +RideEditor::isAnomaly(int row, int col) +{ + if (row < 0 || col < 0) return false; + + if (data->anomalies.value(xsstring(row,model->columnType(col)), "") != "") + return true; + + return false; +} + +bool +RideEditor::isFound(int row, int col) +{ + if (row < 0 || col < 0) return false; + + if (data->found.value(xsstring(row,model->columnType(col)), "") != "") + return true; + + return false; +} + +bool +RideEditor::isTooPrecise(int row, int column) +{ + if (row < 0 || column < 0) return false; + + RideFile::SeriesType what = model->columnType(column); + int dp; + QString value = QString("%1").arg(getValue(row, column), 0, 'g', 10); + + if ((dp = value.indexOf(".")) >= 0) + if (value.length()-(dp+1) > RideFile::decimalsFor(what)) return true; + + return false; +} + +bool +RideEditor::isRowSelected() +{ + QList selection = table->selectionModel()->selection().indexes(); + + if (selection.count() > 0 && + selection[0].column() == 0 && + selection[selection.count()-1].column() == (model->columnCount()-1)) + return true; + + return false; +} + +bool +RideEditor::isColumnSelected() +{ + QList selection = table->selectionModel()->selection().indexes(); + + if (selection.count() > 0 && + selection[0].row() == 0 && + selection[selection.count()-1].row() == (ride->ride()->dataPoints().count()-1)) + return true; + + return false; +} + + +//---------------------------------------------------------------------- +// Toolbar functions +//---------------------------------------------------------------------- +void +RideEditor::saveFile() +{ + if (ride->isDirty()) { + main->saveRideSingleDialog(ride); + } +} + +void +RideEditor::undo() +{ + ride->ride()->command->undoCommand(); +} + +void +RideEditor::redo() +{ + ride->ride()->command->redoCommand(); +} + +void +RideEditor::find() +{ + // look for a value in a range and allow user to next/previous across + //RideEditorFindDialog finder(this, table); + //finder.exec(); + FindDialog *finder = new FindDialog(this); + + // close when a new ride is selected + connect(main, SIGNAL(rideSelected()), finder, SLOT(close())); + finder->show(); +} + +void +RideEditor::check() +{ + // run through all the available channels and find anomalies + data->anomalies.clear(); + + QVector power; + QVector secs; + double lastdistance; + int count = 0; + + foreach (RideFilePoint *point, ride->ride()->dataPoints()) { + power.append(point->watts); + secs.append(point->secs); + + if (count) { + + // whilst we are here we might as well check for gaps in recording + // anything bigger than a second is of a material concern + // and we assume time always flows forward ;-) + double diff = secs[count] - (secs[count-1] + ride->ride()->recIntSecs()); + if (diff > (double)1.0 || diff < (double)-1.0 || secs[count] < secs[count-1]) { + data->anomalies.insert(xsstring(count, RideFile::secs), + tr("Invalid recording gap")); + } + + // and on the same theme what about distance going backwards? + if (point->km < lastdistance) + data->anomalies.insert(xsstring(count, RideFile::km), + tr("Distance goes backwards.")); + + } + lastdistance = point->km; + + // suspicious values + if (point->cad > 150) { + data->anomalies.insert(xsstring(count, RideFile::cad), + tr("Suspiciously high cadence")); + } + if (point->hr > 200) { + data->anomalies.insert(xsstring(count, RideFile::hr), + tr("Suspiciously high heartrate")); + } + if (point->kph > 100) { + data->anomalies.insert(xsstring(count, RideFile::kph), + tr("Suspiciously high speed")); + } + if (point->lat > 90 || point->lat < -90) { + data->anomalies.insert(xsstring(count, RideFile::lat), + tr("Out of bounds value")); + } + if (point->lon > 180 || point->lon < -180) { + data->anomalies.insert(xsstring(count, RideFile::lon), + tr("Out of bounds value")); + } + if (ride->ride()->areDataPresent()->cad && point->nm && !point->cad) { + data->anomalies.insert(xsstring(count, RideFile::nm), + tr("Non-zero torque but zero cadence")); + + } + count++; + } + + // lets look at the Power Column if its there and has enough data + int column = model->headings().indexOf(tr("Power")); + if (column >= 0 && ride->ride()->dataPoints().count() >= 30) { + + + LTMOutliers *outliers = new LTMOutliers(secs.data(), power.data(), power.count(), 30, false); + + // run through the ranked list + for (int i=0; igetDeviationForRank(i) < DPFSvariance) break; + + // ok, so its highly variant but is it over + // the max value we are willing to accept? + if (outliers->getYForRank(i) < DPFSmax) continue; + + // which one is it + data->anomalies.insert(xsstring(outliers->getIndexForRank(i), RideFile::watts), tr("Data spike candidate")); + } + } + + // redraw - even if no anomalies were found since + // some may have been highlighted previouslt. This is + // an expensive operation, but then so is the check() + // function. + model->forceRedraw(); +} + +//---------------------------------------------------------------------- +// handle TableView signals / events +//---------------------------------------------------------------------- +bool +RideEditor::eventFilter(QObject *object, QEvent *e) +{ + // not for the table? + if (object != (QObject *)table) return false; + + // what happenned? + switch(e->type()) + { + case QEvent::ContextMenu: + borderMenu(((QMouseEvent *)e)->pos()); + return true; // I'll take that thanks + break; + + case QEvent::KeyPress: + { + QKeyEvent *keyEvent = static_cast(e); + if (keyEvent->modifiers() & Qt::ControlModifier) { + switch (keyEvent->key()) { + + case Qt::Key_C: // defacto standard for copy + copy(); + return true; + + case Qt::Key_V: // defacto standard for paste + paste(); + return true; + + case Qt::Key_X: // defacto standard for cut + cut(); + return true; + + case Qt::Key_Y: // emerging standard for redo + redo(); + return true; + + case Qt::Key_Z: // common standard for undo + undo(); + return true; + + case Qt::Key_0: + clear(); + return true; + + default: + return false; + } + } + break; + } + + default: + break; + } + return false; +} + +void +RideEditor::stdContextMenu(QMenu *menu, const QPoint &pos) +{ + int row = table->indexAt(pos).row(); + int column = table->indexAt(pos).column(); + + QIcon undoIcon(":images/toolbar/undo.png"); + QIcon redoIcon(":images/toolbar/redo.png"); + QIcon cutIcon(":images/toolbar/cut.png"); + QIcon pasteIcon(":images/toolbar/paste.png"); + QIcon copyIcon(":images/toolbar/copy.png"); + + bool pastable = QApplication::clipboard()->text() == "" ? false : true; + + // setup all the actions + QAction *undoAct = new QAction(undoIcon, tr("Undo"), table); + undoAct->setShortcut(QKeySequence("Ctrl+Z")); + undoAct->setEnabled(ride->ride()->command->undoCount() > 0); + menu->addAction(undoAct); + connect(undoAct, SIGNAL(triggered()), this, SLOT(undo())); + + QAction *redoAct = new QAction(redoIcon, tr("Redo"), table); + redoAct->setShortcut(QKeySequence("Ctrl+Y")); + redoAct->setEnabled(ride->ride()->command->redoCount() > 0); + menu->addAction(redoAct); + connect(redoAct, SIGNAL(triggered()), this, SLOT(redo())); + + menu->addSeparator(); + + QAction *cutAct = new QAction(cutIcon, tr("Cut"), table); + cutAct->setShortcut(QKeySequence("Ctrl+X")); + cutAct->setEnabled(isRowSelected() || isColumnSelected()); + menu->addAction(cutAct); + connect(cutAct, SIGNAL(triggered()), this, SLOT(cut())); + + QAction *copyAct = new QAction(copyIcon, tr("Copy"), table); + copyAct->setShortcut(QKeySequence("Ctrl+C")); + copyAct->setEnabled(true); + menu->addAction(copyAct); + connect(copyAct, SIGNAL(triggered()), this, SLOT(copy())); + + QAction *pasteAct = new QAction(pasteIcon, tr("Paste"), table); + pasteAct->setShortcut(QKeySequence("Ctrl+V")); + pasteAct->setEnabled(pastable); + menu->addAction(pasteAct); + connect(pasteAct, SIGNAL(triggered()), this, SLOT(paste())); + + QAction *specialAct = new QAction(tr("Paste Special..."), table); + specialAct->setEnabled(pastable); + menu->addAction(specialAct); + connect(specialAct, SIGNAL(triggered()), this, SLOT(pasteSpecial())); + + QAction *clearAct = new QAction(tr("Clear Contents"), table); + clearAct->setShortcut(QKeySequence("Ctrl+0")); + clearAct->setEnabled(true); + menu->addAction(clearAct); + connect(clearAct, SIGNAL(triggered()), this, SLOT(clear())); + + currentCell.row = row; + currentCell.column = column; +} + +void +RideEditor::cellMenu(const QPoint &pos) +{ + + int row = table->indexAt(pos).row(); + int column = table->indexAt(pos).column(); + bool anomaly = isAnomaly(row, column); + + QMenu menu(table); + stdContextMenu(&menu, pos); + + menu.addSeparator(); + + QAction *smoothAnomaly = new QAction(tr("Smooth Anomaly"), table); + smoothAnomaly->setEnabled(anomaly); + menu.addAction(smoothAnomaly); + connect(smoothAnomaly, SIGNAL(triggered(void)), this, SLOT(smooth())); + + currentCell.row = row < 0 ? 0 : row; + currentCell.column = column < 0 ? 0 : column; + menu.exec(table->mapToGlobal(QPoint(pos.x(), pos.y()+20))); +} + + +void +RideEditor::borderMenu(const QPoint &pos) +{ + + int column, row; + + // but we need to set the row or column to zero since + // we are in the border, this seems an easy and quick way + // to do this (the indexAt function assumes pos starts from + // 0 for row/col 0 and does not include the vertical + // or horizontal header width (which is what we go passed) + if (pos.y() <= table->horizontalHeader()->height()) { + row = 0; + QPoint tickle(pos.x() - table->verticalHeader()->width(), + pos.y()); + column = table->indexAt(tickle).column(); + } + if (pos.x() <= table->verticalHeader()->width()) { + column = 0; + QPoint tickle(pos.x(), pos.y() - table->horizontalHeader()->height()); + row = table->indexAt(tickle).row(); + } + + // avoid crash when pos translation failed + // just return with no menu options added + if (row < 0 && column < 0) return; + + QMenu menu(table); + stdContextMenu(&menu, pos); + + menu.addSeparator(); + + if (column <= 0) { + + QAction *delAct = new QAction(tr("Delete Row"), table); + delAct->setEnabled(isRowSelected()); + menu.addAction(delAct); + connect(delAct, SIGNAL(triggered()), this, SLOT(delRow())); + + QAction *insAct = new QAction(tr("Insert Row"), table); + insAct->setEnabled(true); + menu.addAction(insAct); + connect(insAct, SIGNAL(triggered()), this, SLOT(insRow())); + + } else if (row <= 0){ + + QAction *delAct = new QAction(tr("Remove Column"), table); + delAct->setEnabled(isColumnSelected()); + menu.addAction(delAct); + connect(delAct, SIGNAL(triggered()), this, SLOT(delColumn())); + + QMenu *insCol = new QMenu(tr("Add Column"), table); + insCol->setEnabled(true); + + // add menu options for each column + if (colMapper) delete colMapper; + colMapper = new QSignalMapper(this); + connect(colMapper, SIGNAL(mapped(const QString &)), this, SLOT(insColumn(const QString &))); + + foreach(QString heading, whatColumns()) { + QAction *insColAct = new QAction(heading, table); + connect(insColAct, SIGNAL(triggered()), colMapper, SLOT(map())); + insColAct->setEnabled(!model->headings().contains(heading)); + insCol->addAction(insColAct); + + // map action to column heading + colMapper->setMapping(insColAct, heading); + } + menu.addMenu(insCol); + } + + currentCell.row = row < 0 ? 0 : row; + currentCell.column = column < 0 ? 0 : column; + menu.exec(table->mapToGlobal(QPoint(pos.x(), pos.y()))); +} + +//---------------------------------------------------------------------- +// Context menu actions +//---------------------------------------------------------------------- +void +RideEditor::copy() +{ + QString copy; + QList selection = table->selectionModel()->selection().indexes(); + + if (selection.count() > 0) { + QString text; + for (int row = selection[0].row(); row <= selection[selection.count()-1].row(); row++) { + + for (int column = selection[0].column(); column <= selection[selection.count()-1].column(); column++) { + if (column == selection[selection.count()-1].column()) + text += QString("%1").arg(getValue(row,column)); + else + text += QString("%1\t").arg(getValue(row,column)); + } + text += "\n"; + } + QApplication::clipboard()->setText(text); + + // remember the headings we copied, so we can + // default them if we paste special them back + copyHeadings.clear(); + for (int column = selection[0].column(); column <= selection[selection.count()-1].column(); column++) + copyHeadings << model->headings()[column]; + } +} + +void +RideEditor::cut() +{ + copy(); + if (isRowSelected()) delRow(); + else if (isColumnSelected()) delColumn(); +} + +void +RideEditor::delRow() +{ + // run through the selected rows and zap them + bool changed = false; + QList selection = table->selectionModel()->selection().indexes(); + + if (selection.count() > 0) { + + changed = true; + + // delete from table - we do in one hit since row-by-row is VERY slow + ride->ride()->command->startLUW("Delete Rows"); + model->removeRows(selection[0].row(), + selection[selection.count()-1].row() - selection[0].row() + 1, QModelIndex()); + ride->ride()->command->endLUW(); + + } +} + +void +RideEditor::delColumn() +{ + // run through the selected columns and "zap" them + bool changed = false; + QList selection = table->selectionModel()->selection().indexes(); + + if (selection.count() > 0) { + + changed = true; + + // Delete each column by its SeriesType + ride->ride()->command->startLUW("Delete Columns"); + for(int column = selection.first().column(), + count = selection.last().column() - selection.first().column() + 1; + count > 0 ; count--) + model->removeColumn(model->columnType(column)); + ride->ride()->command->endLUW(); + } +} + +void +RideEditor::insRow() +{ + // add to model + model->insertRow(currentCell.row, QModelIndex()); +} + +void +RideEditor::insColumn(QString name) +{ + // update state data + RideFile::SeriesType series; + + if (name == tr("Time")) series = RideFile::secs; + if (name == tr("Distance")) series = RideFile::km; + if (name == tr("Power")) series = RideFile::watts; + if (name == tr("Cadence")) series = RideFile::cad; + if (name == tr("Speed")) series = RideFile::kph; + if (name == tr("Torque")) series = RideFile::nm; + if (name == tr("Latitude")) series = RideFile::lat; + if (name == tr("Longitude")) series = RideFile::lon; + if (name == tr("Altitude")) series = RideFile::alt; + if (name == tr("Headwind")) series = RideFile::headwind; + if (name == tr("Interval")) series = RideFile::interval; + if (name == tr("Heartrate")) series = RideFile::hr; + + model->insertColumn(series); +} + +void +RideEditor::paste() +{ + QVector > cells; + QStringList seps, head; + seps << "\t"; + + getPaste(cells, seps, head, false); + + // empty paste buffer + if (cells.count() == 0 || cells[0].count() == 0) return; + + // if selected range is not the same + // size as the copy buffer then barf + // unless just a single cell selected + QList selection = table->selectionModel()->selection().indexes(); + + // is anything selected? + if (selection.count() == 0) { + // wrong size + QMessageBox oops(QMessageBox::Critical, tr("Paste error"), + tr("Please select target cell or cells to paste values into.")); + oops.exec(); + return; + } + + int selectedrow = selection[0].row(); + int selectedcol = selection[0].column(); + int selectedrows = selection[selection.count()-1].row() - selectedrow + 1; + int selectedcols = selection[selection.count()-1].column() - selectedcol + 1; + + if (selection.count() > 1 && + (selectedrows != cells.count() || selectedcols != cells[0].count())) { + + // wrong size + QMessageBox oops(QMessageBox::Critical, tr("Paste error"), + tr("Copy buffer and selected area are diffferent sizes.")); + oops.exec(); + return; + } + + // overrun cols? + if (selection.count() == 1 && + (selectedcol + cells[0].count()) > model->columnCount()) { + QMessageBox oops(QMessageBox::Critical, tr("Paste error"), + tr("Copy buffer has more columns than available.")); + oops.exec(); + return; + } + + // overrun rows? + if (selection.count() == 1 && + (selectedrow + cells.count()) > ride->ride()->dataPoints().count()) { + QMessageBox oops(QMessageBox::Critical, tr("Paste error"), + tr("Copy buffer has more rows than available.")); + oops.exec(); + return; + } + + // go paste! + ride->ride()->command->startLUW("Paste Cells"); + for (int i=0; i ride->ride()->dataPoints().count()-1) break; + for(int j=0; j model->columnCount()-1)) break; + + // set table + setModelValue(selectedrow+i, selectedcol+j, cells[i][j]); + } + } + ride->ride()->command->endLUW(); +} + +// get clipboard into a 2-dim array of doubles +void +RideEditor::getPaste(QVector >&cells, QStringList &seps, QStringList &head, bool hasHeader) +{ + QString text = QApplication::clipboard()->text(); + + int row = 0; + int col = 0; + bool first = true; + + QString regexpStr; + regexpStr = "["; + foreach (QString sep, seps) regexpStr += sep; + regexpStr += "]"; + QRegExp sep(regexpStr); // RegExp for seperators + + foreach(QString line, text.split("\n")) { + if (line == "") continue; + + if (hasHeader && first == true) { + foreach (QString token, line.split(sep)) { + head << token; + } + } else { + cells.resize(row+1); + foreach (QString token, line.split(sep)) { + cells[row].resize(col+1); + cells[row][col] = token.toDouble(); + col++; + + // if there are more cols than in the + // heading row then set to unknown + while (hasHeader && (col+1) > head.count()) + head << "unknown"; + } + row++; + col = 0; + } + first = false; + } +} + +void +RideEditor::pasteSpecial() +{ + // paste clipboard but with a dialog to + // choose field columns and insert rather + // than overwrite or append rather than insert + + PasteSpecialDialog *paster = new PasteSpecialDialog(this); + + // center the dialog + QDesktopWidget *desktop = QApplication::desktop(); + int x = (desktop->width() - paster->size().width()) / 2; + int y = ((desktop->height() - paster->size().height()) / 2) -50; + + // move window to desired coordinates + paster->move(x,y); + paster->exec(); +} + +void +RideEditor::clear() +{ + bool changed = false; + + // Set the selected cells to zero + ride->ride()->command->startLUW("Clear cells"); + foreach (QModelIndex current, table->selectionModel()->selection().indexes()) { + changed = true; + setModelValue(current.row(), current.column(), (double)0.0); + } + ride->ride()->command->endLUW(); +} + +void +RideEditor::smooth() +{ + QString xs = xsstring(currentCell.row, model->columnType(currentCell.column)); + + // calculate smoothed value + double left = 0.0; + double right = 0.0; + if (currentCell.row > 0) left = getValue(currentCell.row-1, currentCell.column); + if (currentCell.row < (ride->ride()->dataPoints().count()-1)) right = getValue(currentCell.row+1, currentCell.column); + double value = (left+right) / 2; + + // update model + setModelValue(currentCell.row, currentCell.column, value); +} + + +//---------------------------------------------------------------------- +// Cell item delegate +//---------------------------------------------------------------------- + +// Cell editor - item delegate +CellDelegate::CellDelegate(RideEditor *rideEditor, QObject *parent) : QItemDelegate(parent), rideEditor(rideEditor) {} + +// setup editor for edit of field!! +QWidget *CellDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const +{ + // what are we editing? + RideFile::SeriesType what = rideEditor->model->columnType(index.column()); + + if (what == RideFile::secs) { + + QTimeEdit *timeEdit = new QTimeEdit(parent); + timeEdit->setDisplayFormat ("hh:mm:ss.zzz"); + connect(timeEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseEditor())); + return timeEdit; + + } else { + QDoubleSpinBox *valueEdit = new QDoubleSpinBox(parent); + valueEdit->setDecimals(RideFile::decimalsFor(what)); + valueEdit->setMaximum(RideFile::maximumFor(what)); + valueEdit->setMinimum(RideFile::minimumFor(what)); + connect(valueEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseEditor())); + return valueEdit; + } +} + +// user hit tab or return so save away the data to our model +void CellDelegate::commitAndCloseEditor() +{ + QDoubleSpinBox *editor = qobject_cast(sender()); + emit commitData(editor); + emit closeEditor(editor); +} + +// We don't set anything because the data is saved within the view not the model! +void CellDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + // what are we editing? + RideFile::SeriesType what = rideEditor->model->columnType(index.column()); + + if (what == RideFile::secs) { + + int seconds, msecs; + secsMsecs(index.model()->data(index, Qt::DisplayRole).toDouble(), seconds, msecs); + + QTime value = QTime(0,0,0,0).addSecs(seconds).addMSecs(msecs); + QTimeEdit *timeEdit = qobject_cast(editor); + timeEdit->setTime(value); + + } else { + QDoubleSpinBox *valueEdit = qobject_cast(editor); + double value = index.model()->data(index, Qt::DisplayRole).toString().toDouble(); + valueEdit->setValue(value); + } +} + +void CellDelegate::updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, + const QModelIndex &/*index*/) const +{ + if (editor) editor->setGeometry(option.rect); +} + +// We don't set anything because the data is saved within the view not the model! +void CellDelegate::setModelData(QWidget *editor, QAbstractItemModel *, const QModelIndex &index) const +{ + // what are we editing? + RideFile::SeriesType what = rideEditor->model->columnType(index.column()); + + if (what == RideFile::secs) { + + double seconds; + QTime midnight(0,0,0,0); + QTimeEdit *timeEdit = qobject_cast(editor); + seconds = (double)midnight.secsTo(timeEdit->time()) + (double)timeEdit->time().msec() / (double)1000.00; + rideEditor->setModelValue(index.row(), index.column(), seconds); + + } else { + QDoubleSpinBox *valueEdit = qobject_cast(editor); + QString value = QString("%1").arg(valueEdit->value()); + rideEditor->setModelValue(index.row(), index.column(), valueEdit->value()); + } +} + +// anomalies are underlined in red, otherwise straight paintjob +void CellDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + // what are we editing? + RideFile::SeriesType what = rideEditor->model->columnType(index.column()); + QString value; + + if (what == RideFile::secs) { + int seconds, msecs; + secsMsecs(index.model()->data(index, Qt::DisplayRole).toDouble(), seconds, msecs); + value = QTime(0,0,0,0).addSecs(seconds).addMSecs(msecs).toString("hh:mm:ss.zzz"); + } else + value = index.model()->data(index, Qt::DisplayRole).toString(); + + // best place to update the tooltip is here, rather than whenever we update the editor + // data, since this is just before it is used... + rideEditor->model->setToolTip(index.row(), rideEditor->model->columnType(index.column()), + rideEditor->data->anomalies.value(xsstring(index.row(), rideEditor->model->columnType(index.column())),"")); + + // found items in yellow + if (rideEditor->isFound(index.row(), index.column()) == true) { + painter->fillRect(option.rect, QBrush(QColor(255,255,0))); + } + + if (rideEditor->isAnomaly(index.row(), index.column())) { + + // wavy line is a pain! + QTextDocument *meh = new QTextDocument(QString(value)); + QTextCharFormat wavy; + wavy.setUnderlineStyle(QTextCharFormat::WaveUnderline); + wavy.setUnderlineColor(Qt::red); + QTextCursor cur = meh->find(value); + cur.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); + cur.selectionStart(); + cur.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + cur.selectionEnd(); + cur.setCharFormat(wavy); + + // only red background if not selected + //if (rideEditor->table->selectionModel()->isSelected(index) == false) + // painter->fillRect(option.rect, QBrush(QColor(255,230,230))); + + painter->save(); + painter->translate(option.rect.x(), option.rect.y()); + meh->drawContents(painter); + painter->restore(); + delete meh; + } else { + + // normal render + QStyleOptionViewItem myOption = option; + myOption.displayAlignment = Qt::AlignLeft | Qt::AlignVCenter; + drawDisplay(painter, myOption, myOption.rect, value); + drawFocus(painter, myOption, myOption.rect); + } + + // warning triangle - for high precision numbers + if (rideEditor->isTooPrecise(index.row(), index.column())) { + QPolygon triangle(3); + triangle.putPoints(0, 3, option.rect.x(), option.rect.y(), + option.rect.x()+4, option.rect.y(), + option.rect.x(), option.rect.y()+4); + painter->setBrush(QBrush(QColor(Qt::darkGreen))); + painter->setPen(QPen(QColor(Qt::darkGreen))); + painter->drawPolygon(triangle); + } +} + +//---------------------------------------------------------------------- +// handle GC Signals +//---------------------------------------------------------------------- +void +RideEditor::intervalSelected() +{ + // is it for the ride item we are editing? + if (main->currentRideItem() == ride) { + + // clear all selections + table->selectionModel()->select(QItemSelection(model->index(0,0), + model->index(ride->ride()->dataPoints().count(),model->columnCount()-1)), + QItemSelectionModel::Clear); + + // highlight selection and jump to last + foreach(QTreeWidgetItem *x, main->allIntervalItems()->treeWidget()->selectedItems()) { + + IntervalItem *current = (IntervalItem*)x; + + if (current != NULL && current->isSelected() == true) { + + // what is the first dataPoint index for this interval? + int start = ride->ride()->timeIndex(current->start); + int end = ride->ride()->timeIndex(current->stop); + + // select all the rows + table->selectionModel()->clearSelection(); + table->selectionModel()->setCurrentIndex(model->index(start,0), QItemSelectionModel::Select); + table->selectionModel()->select(QItemSelection(model->index(start,0), + model->index(end,model->columnCount()-1)), + QItemSelectionModel::Select); + } + } + } +} + +void +RideEditor::rideSelected() +{ + RideItem *current = main->rideItem(); + if (!current) return; + + ride = current; + + // get/or setup ridefile state data + if (ride->ride()->editorData() == NULL) { + data = new EditorData; + ride->ride()->setEditorData(data); + } else { + data = ride->ride()->editorData(); + data->found.clear(); // search is not active, so clear + } + + model->setRide(ride->ride()); + + // reset the save icon on the toolbar + if (ride->isDirty()) saveAct->setEnabled(true); + else saveAct->setEnabled(false); + + // connect the ride command signals so we can + // set/reset undo/redo + static QPointer connection = NULL; + if (connection) { + disconnect(connection, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*))); + disconnect(connection, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*))); + } + connection = ride->ride()->command; + connect(connection, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*))); + connect(connection, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*))); + + // lets set them anyway + if (ride->ride()->command->redoCount() == 0) redoAct->setEnabled(false); + else redoAct->setEnabled(true); + if (ride->ride()->command->undoCount() == 0) undoAct->setEnabled(false); + else undoAct->setEnabled(true); + + // look for anomalies + check(); +} + +// We update the current selection on the table view +// to reflect the actions performed on the data. This +// is especially relevant when 'redo'ing a command +// since it signposts the user to the change that has +// been applied and makes the UI feel more 'natural' +// +void +RideEditor::beginCommand(bool, RideCommand *cmd) +{ + // when executing a Logical Unit of Work we + // highlight sells as we go, rather than + // clearing the current selection and highlighting + // only those cells updated in the current command + // we set inLUW to let endCommand (below) know that + // we are in an LUW + if (cmd->type == RideCommand::LUW) { + inLUW = true; + + // redo needs to clear the current selection + // since we do not clear selections during + // a LUW in endCommand below. We do not clear + // for the first 'do' because the current + // selection is being used to identify which + // cells to operate upon. + if (cmd->docount) { + itemselection.clear(); + table->selectionModel()->clearSelection(); + } + } +} + +// Once the command has been executed we update the +// selection model to highlight the changes to the +// user. We also need to update the EditorData maps +// to reflect changes when rows are added or removed +void +RideEditor::endCommand(bool undo, RideCommand *cmd) +{ + + // Update the undo/redo toolbar icons + if (ride->ride()->command->redoCount() == 0) redoAct->setEnabled(false); + else redoAct->setEnabled(true); + if (ride->ride()->command->undoCount() == 0) undoAct->setEnabled(false); + else undoAct->setEnabled(true); + + // update the selection model when a command has been executed + switch (cmd->type) { + + case RideCommand::SetPointValue: + { + SetPointValueCommand *spv = (SetPointValueCommand*)cmd; + + // move cursor to point updated + QModelIndex cursor = model->index(spv->row, model->columnFor(spv->series)); + // NOTE: This is to circumvent a performance issue with multiple + // calls to setCurrentIndex XXX still TODO... + if (inLUW) { // remember and do it at the end -- otherwise major performance impact!! + itemselection << cursor; + } else { + table->selectionModel()->select(cursor, QItemSelectionModel::SelectCurrent); + table->selectionModel()->setCurrentIndex(cursor, inLUW ? QItemSelectionModel::Select : + QItemSelectionModel::SelectCurrent); + } + + break; + } + case RideCommand::InsertPoint: + { + InsertPointCommand *ip = (InsertPointCommand *)cmd; + if (undo) { // deleted this row... + data->deleteRows(ip->row, 1); + } else { + data->insertRows(ip->row, 1); + } + break; + } + case RideCommand::DeletePoint: + { + DeletePointCommand *dp = (DeletePointCommand *)cmd; + if (undo) { + // clear current + if (!inLUW) table->selectionModel()->clearSelection(); + + // undo delete brings in a row to highlight + QModelIndex topleft = model->index(dp->row, 0); + QItemSelection highlight(topleft, model->index(dp->row, model->headings().count()-1)); + + // highlight the rows brought back in + table->selectionModel()->setCurrentIndex(topleft, QItemSelectionModel::Select); + table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : + QItemSelectionModel::SelectCurrent); + + // update the EditorData maps + data->insertRows(dp->row, 1); + } else { + // update the EditorData maps + data->deleteRows(dp->row, 1); + } + break; + } + case RideCommand::DeletePoints: + { + DeletePointsCommand *dp = (DeletePointsCommand *)cmd; + if (undo) { + + // clear current + if (!inLUW) table->selectionModel()->clearSelection(); + + // highlight the rows brought back in + // undo delete brings in a row to highlight + QModelIndex topleft = model->index(dp->row, 0); + QItemSelection highlight(topleft, model->index(dp->row+dp->count-1, model->headings().count()-1)); + + // highlight the rows brought back in + table->selectionModel()->setCurrentIndex(topleft, QItemSelectionModel::Select); + table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : + QItemSelectionModel::SelectCurrent); + // update the EditorData maps + data->insertRows(dp->row, dp->count); + } else { + // update the EditorData maps + data->deleteRows(dp->row, dp->count); + } + break; + } + case RideCommand::AppendPoints: + if (!undo) { + + // clear current + if (!inLUW) table->selectionModel()->clearSelection(); + + // show the user where the rows went + AppendPointsCommand *ap = (AppendPointsCommand*)cmd; + QModelIndex topleft = model->index(ap->row, 0); + QItemSelection highlight(topleft, model->index(ap->row+ap->count-1, model->headings().count()-1)); + + // move cursor and highligth all the rows + table->selectionModel()->setCurrentIndex(topleft, QItemSelectionModel::Select); + table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : + QItemSelectionModel::SelectCurrent); + } + break; + + case RideCommand::SetDataPresent: + { + // clear current + if (!inLUW) table->selectionModel()->clearSelection(); + + // show the user where the rows went + SetDataPresentCommand *sp = (SetDataPresentCommand*)cmd; + + // highlight row just arrived + if ((sp->oldvalue == true && undo == true) || (sp->oldvalue == false && undo == false)) { + QModelIndex top = model->index(0, model->columnFor(sp->series)); + QModelIndex bottom = model->index(ride->ride()->dataPoints().count()-1, model->columnFor(sp->series)); + QItemSelection highlight(top,bottom); + + // move cursor and highligth all the rows + if (!inLUW) { + table->selectionModel()->clearSelection(); + table->selectionModel()->setCurrentIndex(top, QItemSelectionModel::Select); + } + table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : + QItemSelectionModel::SelectCurrent); + } + + } + break; + + case RideCommand::LUW: + { + inLUW = false; + + // kinda crap, but QItemSelection::merge was painfully slow + // and we cannot guarantee that a LUW will be in a contiguous + // range since it collects lots of atomic actions + int top=99999999, left=99999999, right=-9999999, bottom=-999999; + foreach (QModelIndex index, itemselection) { + if (index.row() < top) top = index.row(); + if (index.row() > bottom) bottom = index.row(); + if (index.column() < left) left = index.column(); + if (index.column() > right) right = index.column(); + } + itemselection.clear(); + table->selectionModel()->select(QItemSelection(model->index(top,left), + model->index(bottom,right)), QItemSelectionModel::Select); + } + break; + + default: + break; + } + if (!inLUW) check(); // refresh the anomalies... +} + +void +RideEditor::rideClean() +{ + // the file was saved / reverted + saveAct->setEnabled(false); +} + +void +RideEditor::rideDirty() +{ + // the file was updated + saveAct->setEnabled(true); +} + +//---------------------------------------------------------------------- +// EditorData functions +//---------------------------------------------------------------------- +void +EditorData::deleteRows(int row, int count) +{ + + // anomalies + if (anomalies.count()) { + QMutableMapIterator a(anomalies); + QMap newa; // updated QMap + while (a.hasNext()) { + a.next(); + + int crow; + RideFile::SeriesType series; + unxsstring(a.key(), crow, series); + + if (crow >= row && crow <= (row+count-1)) { + // do nothing - i.e. don't copy across - it is zapped + ; + } else if (crow > (row+count-1)) { + crow -= count; + newa.insert(xsstring(crow,series), a.value()); + } else { + newa.insert(a.key(), a.value()); + } + } + anomalies = newa; // replace with resynced values + } + + // found + if (found.count()) { + QMutableMapIterator r(found); + QMap newr; // updated QMap + while (r.hasNext()) { + r.next(); + + int crow; + RideFile::SeriesType series; + unxsstring(r.key(), crow, series); + + if (crow >= row && crow <= (row+count-1)) { + // do nothing - i.e. don't copy across + ; + } else if (crow > (row+count-1)) { + crow -= count; + newr.insert(xsstring(crow,series), r.value()); + } else { + newr.insert(r.key(), r.value()); + } + } + found = newr; // replace with resynced values + } +} + +void +EditorData::deleteSeries(RideFile::SeriesType series) +{ + + // anomalies + if (anomalies.count()) { + QMutableMapIterator a(anomalies); + QMap newa; // updated QMap + while (a.hasNext()) { + a.next(); + + int crow; + RideFile::SeriesType cseries; + unxsstring(a.key(), crow, cseries); + + if (cseries == series) { + // do nothing - i.e. don't copy across - it is zapped + ; + } else { + newa.insert(a.key(), a.value()); + } + } + anomalies = newa; // replace with resynced values + } + + // found + if (found.count()) { + QMutableMapIterator r(found); + QMap newr; // updated QMap + while (r.hasNext()) { + r.next(); + + int crow; + RideFile::SeriesType cseries; + unxsstring(r.key(), crow, cseries); + + if (cseries == series) { + // do nothing - i.e. don't copy across + ; + } else { + newr.insert(r.key(), r.value()); + } + } + found = newr; // replace with resynced values + } +} + +void +EditorData::insertRows(int row, int count) +{ + + // anomalies + if (anomalies.count()) { + QMutableMapIterator a(anomalies); + QMap newa; // updated QMap + while (a.hasNext()) { + a.next(); + + int crow; + RideFile::SeriesType series; + unxsstring(a.key(), crow, series); + + if (crow > row) { + crow += count; + newa.insert(xsstring(crow,series), a.value()); + } else { + newa.insert(a.key(), a.value()); + } + } + anomalies = newa; // replace with resynced values + } + + // found + if (found.count()) { + QMutableMapIterator r(found); + QMap newr; // updated QMap + + while (r.hasNext()) { + r.next(); + + int crow; + RideFile::SeriesType series; + unxsstring(r.key(), crow, series); + + if (crow > row) { + crow += count; + newr.insert(xsstring(crow,series), r.value()); + } else { + newr.insert(r.key(), r.value()); + } + } + found = newr; // replace with resynced values + } +} + +//---------------------------------------------------------------------- +// Toolbar dialogs +//---------------------------------------------------------------------- + +// +// Find Dialog +// +FindDialog::FindDialog(RideEditor *rideEditor, QWidget *parent) : QDialog(parent), rideEditor(rideEditor) +{ + // setup the basic window settings; nonmodal, ontop and delete on close + setWindowTitle("Search"); + setAttribute(Qt::WA_DeleteOnClose); + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::Tool); + + + // create UI components + QLabel *look = new QLabel(tr("Find values"), this); + type = new QComboBox(this); + type->addItem(tr("Between")); + type->addItem(tr("Not Between")); + type->addItem(tr("Greater Than")); + type->addItem(tr("Less Than")); + type->addItem(tr("Equal To")); + type->addItem(tr("Not Equal To")); + + andLabel = new QLabel(tr("and"), this); + from = new QDoubleSpinBox(this); + from->setMinimum(-9999999); + from->setMaximum(9999999); + from->setDecimals(5); + from->setSingleStep(0.00001); + + to = new QDoubleSpinBox(this); + to->setMinimum(-9999999); + to->setMaximum(9999999); + to->setDecimals(5); + to->setSingleStep(0.00001); + + // which columns? + foreach (QString heading, rideEditor->model->headings()) { + QCheckBox *add = new QCheckBox(heading); + if (heading == tr("Power")) + add->setChecked(true); + else + add->setChecked(false); + channels << add; + } + + // buttons + findButton = new QPushButton(tr("Find")); + closeButton = new QPushButton(tr("Close")); + + // results + resultsTable = new QTableWidget(this); + resultsTable->setColumnCount(4); + resultsTable->setColumnHidden(3, true); + resultsTable->setSortingEnabled(true); + QStringList header; + header << "Time" << "Column" << "Value"; + resultsTable->setHorizontalHeaderLabels(header); + resultsTable->verticalHeader()->hide(); + resultsTable->setShowGrid(false); + resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); + resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + + // layout the widget + QVBoxLayout *mainLayout = new QVBoxLayout(this); + QGridLayout *criteria = new QGridLayout; + criteria->setColumnStretch(0,1); + criteria->setColumnStretch(1,2); + + criteria->addWidget(look, 0,0, Qt::AlignLeft|Qt::AlignTop); + criteria->addWidget(type, 0,1, Qt::AlignLeft|Qt::AlignTop); + criteria->addWidget(from, 1,1, Qt::AlignLeft|Qt::AlignTop); + criteria->addWidget(andLabel, 2,0, Qt::AlignRight|Qt::AlignTop); + criteria->addWidget(to, 2,1, Qt::AlignLeft|Qt::AlignTop); + mainLayout->addLayout(criteria); + + QGridLayout *chans = new QGridLayout; + mainLayout->addLayout(chans); + + int row =0; + int col =0; + foreach (QCheckBox *check, channels) { + chans->addWidget(check, row,col); + if (++col > 2) { col =0; row++; } + } + + QHBoxLayout *execute = new QHBoxLayout; + execute->addStretch(); + execute->addWidget(findButton); + mainLayout->addLayout(execute); + + mainLayout->addWidget(resultsTable); + + QHBoxLayout *closer = new QHBoxLayout; + closer->addStretch(); + closer->addWidget(closeButton); + mainLayout->addLayout(closer); + + setLayout(mainLayout); + + connect(type, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged(int))); + connect(findButton, SIGNAL(clicked()), this, SLOT(find())); + connect(closeButton, SIGNAL(clicked()), this, SLOT(accept())); + connect(resultsTable, SIGNAL(itemSelectionChanged()), this, SLOT(selection())); + + // refresh when data changes... + connect(rideEditor->ride->ride()->command, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(dataChanged())); +} + +FindDialog::~FindDialog() +{ + rideEditor->data->found.clear(); + clearResultsTable(); + rideEditor->model->forceRedraw(); +} + +void +FindDialog::typeChanged(int index) +{ + // 0 and 1 are range based the rest are single value + if (index < 2) { + from->show(); + to->show(); + andLabel->show(); + } else { + to->hide(); + andLabel->hide(); + } +} + +void +FindDialog::find() +{ + // are we looking anywhere? + bool search = false; + foreach (QCheckBox *c, channels) if (c->isChecked()) search = true; + if (search == false) return; + + // ok something to do then... + rideEditor->data->found.clear(); + clearResultsTable(); + + for (int i=0; i< rideEditor->ride->ride()->dataPoints().count(); i++) { + // for each selected channel, get the value and + // see if it matches + + foreach (QCheckBox *c, channels) { + + if (c->isChecked()) { + + // which Column? + int col = rideEditor->model->headings().indexOf(c->text()); + if (col >= 0) { + + double value = rideEditor->getValue(i, col); + + bool match = false; + switch(type->currentIndex()) { + + case 0 : // between + if (value >= from->value() && value <= to->value()) match = true; + break; + + case 1 : // not between + if (!(value >= from->value() && value <= to->value())) match = true; + break; + + case 2 : // greater than + if (value > from->value()) match = true; + break; + + case 3 : // less than + if (value < from->value()) match = true; + break; + + case 4 : // matches + if (value == from->value()) match = true; + break; + + case 5 : // not equal + if (value != from->value()) match = true; + break; + + } + + if (match == true) { + + // highlight on the table + rideEditor->data->found.insert(xsstring(i,rideEditor->model->columnType(col)), QString("%1").arg(value)); + + } + } + } + } + } + + dataChanged(); // update results table and redraw + + rideEditor->model->forceRedraw(); +} + +void +FindDialog::dataChanged() +{ + // a column or row was inserted/deleted + // we need to refresh the results table + // to reflect the new values + clearResultsTable(); + + // do not be tempted to removed the following two lines! + // by setting the row count to zero the item selection + // is cleared properly and fixes a crash in the line + // marked ZZZZ below. This only occurs when the current item + // selected is greater than the new row count. + resultsTable->setRowCount(0); // <<< fixes crash at ZZZZ + resultsTable->setColumnCount(0); + + resultsTable->setRowCount(rideEditor->data->found.count()); // <<< ZZZZ + resultsTable->setColumnCount(4); + resultsTable->setColumnHidden(3, true); // has start xystring + QMapIterator f(rideEditor->data->found); + + resultsTable->setSortingEnabled(false);// see QT Bug QTBUG-7483 + + int counter =0; + while (f.hasNext()) { + + f.next(); + + int row; + RideFile::SeriesType series; + unxsstring(f.key(), row, series); + + // time -- format correctly... held as a double in the model + int seconds, msecs; + secsMsecs(rideEditor->model->getValue(row,0), seconds, msecs); + QString value = QTime(0,0,0,0).addSecs(seconds).addMSecs(msecs).toString("hh:mm:ss.zzz"); + + QTableWidgetItem *t = new QTableWidgetItem; + t->setText(value); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + resultsTable->setItem(counter, 0, t); + + // channel + t = new QTableWidgetItem; + t->setText(RideFile::seriesName(series)); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + resultsTable->setItem(counter, 1, t); + + // value + t = new QTableWidgetItem; + t->setText(QString("%1").arg(rideEditor->ride->ride()->getPointValue(row,series))); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + resultsTable->setItem(counter, 2, t); + + // xs for selection + t = new QTableWidgetItem; + t->setText(f.key()); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + resultsTable->setItem(counter, 3, t); + + resultsTable->setRowHeight(counter, 20); + + counter++; + } + resultsTable->setSortingEnabled(true);// see QT Bug QTBUG-7483 + QStringList header; + header << "Time" << "Column" << "Value"; + resultsTable->setHorizontalHeaderLabels(header); +} + +void +FindDialog::close() +{ + accept(); +} + +void +FindDialog::selection() +{ + if (resultsTable->currentRow() < 0) return; + + // jump to the found item in the main table + int row; + RideFile::SeriesType series; + unxsstring(resultsTable->item(resultsTable->currentRow(), 3)->text(), row, series); + rideEditor->table->setCurrentIndex(rideEditor->model->index(row,rideEditor->model->columnFor(series))); +} + +void +FindDialog::clearResultsTable() +{ + resultsTable->selectionModel()->clearSelection(); + // zap the 3 main cols and two hidden ones + for (int i=0; irowCount(); i++) { + for (int j=0; jcolumnCount(); j++) + delete resultsTable->takeItem(i,j); + } +} + +// +// Paste Special Dialog +// +PasteSpecialDialog::PasteSpecialDialog(RideEditor *rideEditor, QWidget *parent) : QDialog(parent), rideEditor(rideEditor) +{ + // setup the basic window settings; nonmodal, ontop and delete on close + setWindowTitle("Paste Special"); + setAttribute(Qt::WA_DeleteOnClose); + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::Tool); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // create the widgets + mode = new QGroupBox(tr("Paste mode")); + separators = new QGroupBox(tr("Separator options")); + contents = new QGroupBox(tr("Columns")); + + append = new QRadioButton(tr("Append")); + overwrite = new QRadioButton(tr("Overwrite")); + + QVBoxLayout *modeLayout = new QVBoxLayout; + modeLayout->addWidget(append); + modeLayout->addWidget(overwrite); + mode->setLayout(modeLayout); + mode->setFlat(false); + append->setChecked(true); + + hasHeader = new QCheckBox(tr("First line has headings")); + tab = new QCheckBox(tr("Tab")); + tab->setChecked(true); + comma = new QCheckBox(tr("Comma")); + semi = new QCheckBox(tr("Semi-colon")); + space = new QCheckBox(tr("Space")); + other = new QCheckBox(tr("Other")); + otherText = new QLineEdit; + QHBoxLayout *otherLayout = new QHBoxLayout; + otherLayout->addWidget(other); + otherLayout->addWidget(otherText); + otherLayout->addStretch(); + + QGridLayout *sepLayout = new QGridLayout; + sepLayout->addWidget(hasHeader, 0,0); + sepLayout->addWidget(tab, 1,0); + sepLayout->addWidget(comma, 1,1); + sepLayout->addWidget(semi, 1,2); + sepLayout->addWidget(space, 1,3); + sepLayout->addLayout(otherLayout, 2,0,1,4); + + separators->setLayout(sepLayout); + separators->setFlat(false); + + // what we got? + seps << "\t"; + rideEditor->getPaste(cells, seps, sourceHeadings, false); + + resultsTable = new QTableView(this); + resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); + resultsTable->setSelectionBehavior(QAbstractItemView::SelectColumns); + QFont font; + font.setPointSize(font.pointSize()-2); // smaller please + resultsTable->setFont(font); + resultsTable->verticalHeader()->setDefaultSectionSize(QFontMetrics(font).height()+2); + + model = new QStandardItemModel; + resultsTable->setModel(model); + setResultsTable(); + + // column selector + QHBoxLayout *selectorLayout = new QHBoxLayout; + QLabel *selectLabel = new QLabel(tr("Column Type")); + columnSelect = new QComboBox; + columnSelect->addItem("Ignore"); + foreach (QString name, rideEditor->model->headings()) + columnSelect->addItem(name); + selectorLayout->addWidget(selectLabel); + selectorLayout->addWidget(columnSelect); + selectorLayout->addStretch(); + + // contents layout + QVBoxLayout *contentsLayout = new QVBoxLayout; + contentsLayout->addLayout(selectorLayout); + contentsLayout->addWidget(resultsTable); + contents->setLayout(contentsLayout); + contents->setFlat(false); + +#if 0 + QDoubleSpinBox *atRow; + QComboBox *textDelimeter; +#endif + + okButton = new QPushButton(tr("OK")); + cancelButton = new QPushButton(tr("Cancel")); + QHBoxLayout *buttons = new QHBoxLayout; + buttons->addStretch(); + buttons->addWidget(cancelButton); + buttons->addWidget(okButton); + + // layout the widgets + QGridLayout *widgetLayout = new QGridLayout; + widgetLayout->addWidget(mode, 0,0); + widgetLayout->addWidget(separators,0,1); + widgetLayout->addWidget(contents,1,0,1,2); + widgetLayout->setRowStretch(0,1); + widgetLayout->setRowStretch(1,4); + + mainLayout->addLayout(widgetLayout); + mainLayout->addLayout(buttons); + + // set size hint + setMinimumHeight(300); + setMinimumWidth(500); + + connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(columnSelect, SIGNAL(currentIndexChanged(int)), this, SLOT(columnChanged())); + connect(resultsTable->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), + this, SLOT(setColumnSelect())); + connect(hasHeader, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); + connect(tab, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); + connect(comma, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); + connect(semi, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); + connect(space, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); + connect(other, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); +} + +void +PasteSpecialDialog::clearResultsTable() +{ + model->clear(); +} + +void +PasteSpecialDialog::setResultsTable() +{ + boost::shared_ptr settings = GetApplicationSettings(); + + model->clear(); + headings.clear(); + + // is it like what was just copied and no headings line? + if (hasHeader->isChecked() == false && cells[0].count() == rideEditor->copyHeadings.count()) { + headings = rideEditor->copyHeadings; + } else { + for (int i=0; hasHeader->isChecked() ? (iisChecked() == true) { + // have we mapped this before? + QString lookup = "colmap/" + sourceHeadings[i]; + QString mapto = settings->value(lookup, "Ignore").toString(); + // is this an available heading tho? + if (columnSelect->findText(mapto) != -1) { + headings << mapto; + } else { + headings << "Ignore"; + } + } else { + headings << "Ignore"; + } + } + } + + model->setRowCount(cells.count() < 50 ? cells.count() : 50); + model->setColumnCount(cells[0].count()); + model->setHorizontalHeaderLabels(headings); + + // just setup the first 50 rows + for (int row=0; row < 50 && row < cells.count(); row++) { + for (int col=0; col < cells[row].count(); col++) { + // add value + model->setItem(row, col, new QStandardItem(QString("%1").arg(cells[row][col]))); + } + } +} + +PasteSpecialDialog::~PasteSpecialDialog() +{ + clearResultsTable(); + rideEditor->model->forceRedraw(); +} + +void +PasteSpecialDialog::okClicked() +{ + // headings has the headings for each column + // with "Ignore" set if we are to igmore it + // cells contains all the actual data from the + // buffer. + // We have three modes; (1) insert means add new + // samples at the top of the current selection + // which will mean all time/distance offsets for + // the remaining rows will be increased and the + // time/distance for the inserted rows will start + // after the previous row time/distance + // (2) append will add all samples to the tail of + // the ride file with time/distance offset from + // the last sample currently in the ride + // (3) overwrite will change the current samples to + // use the non-ignored values, including time/distance + // which may well create out-of-sync values. Which + // is why we do an anomaly check at the end. + + // add these to the pasted rows + double timeOffset, distanceOffset; + int where = rideEditor->ride->ride()->dataPoints().count(); + + if (append->isChecked() && rideEditor->ride->ride()->dataPoints().count()) { + timeOffset = rideEditor->ride->ride()->dataPoints().last()->secs; + distanceOffset = rideEditor->ride->ride()->dataPoints().last()->km; + } + + // if we are inserting or appending lets create an array of rows to insert + if (append->isChecked()) { + + QVector newRows; + + for (int row=0; row < cells.count(); row++) { + + struct RideFilePoint newrow; + newrow.secs = timeOffset; // in case it is being ignore or not available + newrow.km = distanceOffset; // in case it is being ignored or not available + + for (int col=0; col < cells[row].count(); col++) { + if (headings[col] == tr("Ignore")) continue; + + double value; + if (headings[col] == tr("Time")) value = cells[row][col] + timeOffset; + else if (headings[col] == tr("Distance")) value = cells[row][col] + distanceOffset; + else value = cells[row][col]; + + // update the relevant value in the new dataPoint based upon the heading... + if (headings[col] == tr("Time")) newrow.secs = value; + if (headings[col] == tr("Distance")) newrow.km = value; + if (headings[col] == tr("Speed")) newrow.kph = value; + if (headings[col] == tr("Cadence")) newrow.cad = value; + if (headings[col] == tr("Power")) newrow.watts = value; + if (headings[col] == tr("Heartrate")) newrow.hr = value; + if (headings[col] == tr("Torque")) newrow.nm = value; + if (headings[col] == tr("Latitude")) newrow.lat = value; + if (headings[col] == tr("Longitude")) newrow.lon = value; + if (headings[col] == tr("Altitude")) newrow.alt = value; + if (headings[col] == tr("Headwind")) newrow.headwind = value; + if (headings[col] == tr("Interval")) newrow.interval = value; + } + + // add to the list + newRows << newrow; + } + + // ok. we are good to go, so overwrite target with source + rideEditor->ride->ride()->command->startLUW("Paste Special"); + + // now we have an array add to dataPoints + rideEditor->model->appendRows(newRows); + + // highlight the affected cells -- good UI for paste, since it might + // update offscreen (esp. true for our paste append) + QItemSelection highlight = QItemSelection(rideEditor->model->index(where, 0), + rideEditor->model->index(where+newRows.count()-1, rideEditor->model->headings().count()-1)); + + // our job is done. + rideEditor->ride->ride()->command->endLUW(); + + } else { + + // *** The paste special overwrite function is somewhat + // *** "clever", it will only overwrite columns that were + // *** selected in the dialog box amd will only overwrite + // *** existing rows. This makes the paste operation quite + // *** "complicated". We do quite a lot of checks to make + // *** sure the user really mean't to do this... + + // get the target selection + QList selection = rideEditor->table->selectionModel()->selection().indexes(); + if (selection.count() == 0) { + // wrong size + QMessageBox oops(QMessageBox::Critical, tr("Paste error"), + tr("Please select target cell or cells to paste values into.")); + oops.exec(); + accept(); + return; + } + + // to make code more readable, and with an eye to refactoring + // use these vars to describe the range selected in the table + // the target we will paste to (i.e. we may truncate) and the + // range available in the clipboard + struct range { int row, column, rows, columns; } selected, target, source; + bool norange = selection.count() == 1 ? true : false; + bool truncate = false, partial = false; + + // what is selected in the selection model? + selected.row = selection.first().row(); + selected.column = selection.first().column(); + selected.rows = selection.last().row() - selection.first().row() + 1; + selected.columns = selection.last().column() - selection.first().column() + 1; + if (norange) { + // Single cell selected for paste means 'from here' not + // 'into here'. So from here to end of the row is selected + // because we check by series type not column number when + // the paste is performed we will not go out of bounds + selected.columns = rideEditor->model->headings().count() - selected.column; + } + + // what is in the clipboard? + source.row = 0; // not defined, obviously + source.column = 0; + source.rows = cells.count(); + source.columns = 0; + foreach(QString heading, headings) + if (heading != tr("Ignore")) source.columns++; + + // so what is the target? + target.row = selected.row; + target.column = selected.column; + if (norange) { + target.rows = source.rows; + target.columns = source.columns; + } else { + target.rows = selected.rows; + target.columns = selected.columns; + } + // out of bounds for ride? + if (target.row + target.rows > rideEditor->ride->ride()->dataPoints().count()) { + truncate = true; + target.rows = rideEditor->ride->ride()->dataPoints().count() - target.row; + } + // selection smaller than clipboard? + if (source.rows > target.rows) { + truncate = true; + } + // partially fill selected rows ? + if (source.rows < target.rows) { + partial = true; + target.rows = source.rows; + } + // out of bounds for columns? + if (target.column + target.columns - 1 > rideEditor->model->headings().count()) { + truncate = true; + target.columns = rideEditor->model->headings().count() - target.column; + } + // selection smaller than clipboard? + if (source.columns > target.columns) { + truncate = true; + } + // partially fill columnss ? + if (source.columns < target.columns) { + partial = true; + target.columns = source.columns; + } + + // + // Now we have calculated the source and target, lets + // make sure the user agrees ... + // + if (truncate || partial) { + + // we are going to truncate + QMessageBox confirm(QMessageBox::Question, tr("Copy/Paste Mismatch"), + tr("The selected range and available data have " + "different sizes, some data may be lost.\n\n" + "Do you want to continue?"), + QMessageBox::Ok | QMessageBox::Cancel); + + if ((confirm.exec() & QMessageBox::Cancel) != 0) { + accept(); + return; // accept doesn't return. + } + } + + // ok. we are good to go, so overwrite target with source + rideEditor->ride->ride()->command->startLUW("Paste Special"); + + for (int i = 0; i < target.rows; i++) { + + for (int j = 0; j < target.columns; j++) { + + // target column type... + RideFile::SeriesType what = rideEditor->model->columnType(target.column + j); + + // do we have that? + int sourceSeries = headings.indexOf(RideFile::seriesName(what)); + if (sourceSeries != -1) // YES, we have some + rideEditor->ride->ride()->command->setPointValue(target.row+i, what, cells[i][sourceSeries]); + } + } + + // highlight what we did + QItemSelection highlight = QItemSelection(rideEditor->model->index(target.row,target.column), + rideEditor->model->index(target.row+target.rows-1, target.column+target.columns-1)); + + // all done. + rideEditor->ride->ride()->command->endLUW(); + } + accept(); +} + +void +PasteSpecialDialog::cancelClicked() +{ + reject(); +} + +void +PasteSpecialDialog::sepsChanged() +{ + seps.clear(); + cells.clear(); + sourceHeadings.clear(); + + if (tab->isChecked()) seps << "\t"; + if (comma->isChecked()) seps << ","; + if (semi->isChecked()) seps << ";"; + if (space->isChecked()) seps << " "; + if (other->isChecked()) seps << otherText->text(); + + rideEditor->getPaste(cells, seps, sourceHeadings, hasHeader->isChecked()); + setResultsTable(); +} + +void +PasteSpecialDialog::setColumnSelect() +{ + QList selection = resultsTable->selectionModel()->selection().indexes(); + if (selection.count() == 0) return; + int column = selection[0].column(); // which column? + active = true; + columnSelect->setCurrentIndex(columnSelect->findText(headings[column])); + active = false; + +} + +void +PasteSpecialDialog::columnChanged() +{ + if (active) return; + + // is anything selected? + QList selection = resultsTable->selectionModel()->selection().indexes(); + if (selection.count() == 0) return; + + // set column heading + int column = selection[0].column(); + QString text = columnSelect->itemText(columnSelect->currentIndex()); + + // set the headings string + headings[column] = text; + + // now update the results table + model->setHorizontalHeaderLabels(headings); + + // lets remember this mapping if its to a source header + if (hasHeader->isChecked() && headings[column] != "Ignore") { + boost::shared_ptr settings = GetApplicationSettings(); + QString lookup = "colmap/" + sourceHeadings[column]; + settings->setValue(lookup, headings[column]); + } +} diff --git a/src/RideEditor.h b/src/RideEditor.h new file mode 100644 index 000000000..1b5d0a22a --- /dev/null +++ b/src/RideEditor.h @@ -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 + +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 >&cells, + QStringList &seps, QStringList &head, bool); + + protected: + EditorData *data; + RideItem *ride; + RideFileTableModel *model; + QStringList copyHeadings; + + private: + MainWindow *main; + + bool inLUW; + QList itemselection; + + double DPFSmax, DPFSvariance; + + QList whatColumns(); + QSignalMapper *colMapper; + + QToolBar *toolbar; + QAction *saveAct, *undoAct, *redoAct, + *searchAct, *checkAct; + + // state data + struct { int row, column; } currentCell; +}; + +class EditorData +{ + public: + QMap anomalies; + QMap 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 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 > 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 diff --git a/src/RideFile.cpp b/src/RideFile.cpp index d8ad45409..2206cdc20 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -18,6 +18,7 @@ */ #include "RideFile.h" +#include "DataProcessor.h" #include "Settings.h" #include "Units.h" #include @@ -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 settings = GetApplicationSettings();; - + boost::shared_ptr 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 newRows) +{ + dataPoints_ += newRows; +} + +void +RideFile::emitSaved() +{ + emit saved(); +} + +void +RideFile::emitReverted() +{ + emit reverted(); +} + +void +RideFile::emitModified() +{ + emit modified(); +} diff --git a/src/RideFile.h b/src/RideFile.h index 2c9ec6299..086fb31c9 100644 --- a/src/RideFile.h +++ b/src/RideFile.h @@ -25,6 +25,11 @@ #include #include #include +#include + +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 dataPoints_; - RideFileDataPresent dataPresent; - QString deviceType_; - QList 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 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 &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 &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& 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 > 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 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 dataPoints_; + RideFileDataPresent dataPresent; + QString deviceType_; + QList intervals_; QMap tags_; + EditorData *data; + }; struct RideFileReader { @@ -163,4 +217,3 @@ class RideFileFactory { }; #endif // _RideFile_h - diff --git a/src/RideFileCommand.cpp b/src/RideFileCommand.cpp new file mode 100644 index 000000000..29e76a2f8 --- /dev/null +++ b/src/RideFileCommand.cpp @@ -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 +#include + +#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 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 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; itype != 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 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 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 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; ideletePoint(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; +} diff --git a/src/RideFileCommand.h b/src/RideFileCommand.h new file mode 100644 index 000000000..6a46add19 --- /dev/null +++ b/src/RideFileCommand.h @@ -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 +#include +#include +#include +#include +#include +#include + +#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 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 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 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 current); + bool doCommand(); + bool undoCommand(); + + // state + int row; + int count; + QVector 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 points); + bool doCommand(); + bool undoCommand(); + + int row, count; + QVector 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 diff --git a/src/RideFileTableModel.cpp b/src/RideFileTableModel.cpp new file mode 100644 index 000000000..714bb01f7 --- /dev/null +++ b/src/RideFileTableModel.cpp @@ -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 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(QVectornewRows) +{ + 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(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, ""); +} diff --git a/src/RideFileTableModel.h b/src/RideFileTableModel.h new file mode 100644 index 000000000..5695e40c6 --- /dev/null +++ b/src/RideFileTableModel.h @@ -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 + +// +// 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(QVectornewRows); + + // 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 tooltips; + + QStringList headings_; + QVector headingsType; + void setHeadings(RideFile::SeriesType series = RideFile::none); +}; +#endif // _GC_RideFileTableModel_h diff --git a/src/RideItem.cpp b/src/RideItem.cpp index 4a7584517..d311c3597 100644 --- a/src/RideItem.cpp +++ b/src/RideItem.cpp @@ -16,6 +16,7 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include #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(); } } diff --git a/src/RideItem.h b/src/RideItem.h index 578ce3157..b84b94e92 100644 --- a/src/RideItem.h +++ b/src/RideItem.h @@ -20,13 +20,24 @@ #define _GC_RideItem_h 1 #include +#include #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 - diff --git a/src/RideMetadata.cpp b/src/RideMetadata.cpp index 8a0b7d8f5..1ced36b78 100644 --- a/src/RideMetadata.cpp +++ b/src/RideMetadata.cpp @@ -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(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; diff --git a/src/SaveDialogs.cpp b/src/SaveDialogs.cpp index 231cd4b31..79c1a2aa2 100644 --- a/src/SaveDialogs.cpp +++ b/src/SaveDialogs.cpp @@ -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(); } //---------------------------------------------------------------------- diff --git a/src/Settings.h b/src/Settings.h index ff71ae217..8f65bd28e 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -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 #include diff --git a/src/SpecialFields.cpp b/src/SpecialFields.cpp index ce09def4d..3cc74813f 100644 --- a/src/SpecialFields.cpp +++ b/src/SpecialFields.cpp @@ -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) diff --git a/src/WeeklySummaryWindow.cpp b/src/WeeklySummaryWindow.cpp index 5ba1deaee..b3c161606 100644 --- a/src/WeeklySummaryWindow.cpp +++ b/src/WeeklySummaryWindow.cpp @@ -28,10 +28,6 @@ #include #include -// 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), diff --git a/src/application.qrc b/src/application.qrc index ffc0f6465..7448f987a 100644 --- a/src/application.qrc +++ b/src/application.qrc @@ -12,5 +12,14 @@ translations/gc_ja.qm xml/charts.xml xml/metadata.xml + images/toolbar/close-icon.png + images/toolbar/save.png + images/toolbar/search.png + images/toolbar/splash green.png + images/toolbar/cut.png + images/toolbar/copy.png + images/toolbar/paste.png + images/toolbar/undo.png + images/toolbar/redo.png diff --git a/src/images/toolbar/close-icon.png b/src/images/toolbar/close-icon.png new file mode 100644 index 000000000..c49943a71 Binary files /dev/null and b/src/images/toolbar/close-icon.png differ diff --git a/src/images/toolbar/copy.png b/src/images/toolbar/copy.png new file mode 100644 index 000000000..9bce1ab2e Binary files /dev/null and b/src/images/toolbar/copy.png differ diff --git a/src/images/toolbar/cut.png b/src/images/toolbar/cut.png new file mode 100644 index 000000000..95f4087df Binary files /dev/null and b/src/images/toolbar/cut.png differ diff --git a/src/images/toolbar/paste.png b/src/images/toolbar/paste.png new file mode 100644 index 000000000..ed5947da7 Binary files /dev/null and b/src/images/toolbar/paste.png differ diff --git a/src/images/toolbar/redo.png b/src/images/toolbar/redo.png new file mode 100644 index 000000000..3eb7b05c8 Binary files /dev/null and b/src/images/toolbar/redo.png differ diff --git a/src/images/toolbar/refresh.png b/src/images/toolbar/refresh.png new file mode 100644 index 000000000..ed5dc6054 Binary files /dev/null and b/src/images/toolbar/refresh.png differ diff --git a/src/images/toolbar/save.png b/src/images/toolbar/save.png new file mode 100644 index 000000000..448b88580 Binary files /dev/null and b/src/images/toolbar/save.png differ diff --git a/src/images/toolbar/search.png b/src/images/toolbar/search.png new file mode 100644 index 000000000..d6ed446f0 Binary files /dev/null and b/src/images/toolbar/search.png differ diff --git a/src/images/toolbar/splash green.png b/src/images/toolbar/splash green.png new file mode 100644 index 000000000..9669a6af2 Binary files /dev/null and b/src/images/toolbar/splash green.png differ diff --git a/src/images/toolbar/star.png b/src/images/toolbar/star.png new file mode 100644 index 000000000..25ef8b290 Binary files /dev/null and b/src/images/toolbar/star.png differ diff --git a/src/images/toolbar/undo.png b/src/images/toolbar/undo.png new file mode 100644 index 000000000..80cb38665 Binary files /dev/null and b/src/images/toolbar/undo.png differ diff --git a/src/src.pro b/src/src.pro index 80d3dc408..2ad849741 100644 --- a/src/src.pro +++ b/src/src.pro @@ -77,6 +77,7 @@ HEADERS += \ DBAccess.h \ DatePickerDialog.h \ DaysScaleDraw.h \ + DataProcessor.h \ Device.h \ DeviceTypes.h \ DeviceConfiguration.h \ @@ -92,6 +93,7 @@ HEADERS += \ LogTimeScaleEngine.h \ LTMCanvasPicker.h \ LTMChartParser.h \ + LTMOutliers.h \ LTMPlot.h \ LTMSettings.h \ LTMTool.h \ @@ -120,7 +122,10 @@ HEADERS += \ ComputrainerController.h \ RealtimePlot.h \ RideCalendar.h \ + RideEditor.h \ RideFile.h \ + RideFileCommand.h \ + RideFileTableModel.h \ RideImportWizard.h \ RideItem.h \ RideMetadata.h \ @@ -174,6 +179,7 @@ SOURCES += \ CriticalPowerWindow.cpp \ CsvRideFile.cpp \ DBAccess.cpp \ + DataProcessor.cpp \ DatePickerDialog.cpp \ Device.cpp \ DeviceTypes.cpp \ @@ -182,6 +188,10 @@ SOURCES += \ ErgFile.cpp \ ErgFilePlot.cpp \ FitRideFile.cpp \ + FixGaps.cpp \ + FixGPS.cpp \ + FixSpikes.cpp \ + FixTorque.cpp \ GcRideFile.cpp \ GoogleMapControl.cpp \ HistogramWindow.cpp \ @@ -190,6 +200,7 @@ SOURCES += \ LogTimeScaleEngine.cpp \ LTMCanvasPicker.cpp \ LTMChartParser.cpp \ + LTMOutliers.cpp \ LTMPlot.cpp \ LTMSettings.cpp \ LTMTool.cpp \ @@ -219,7 +230,10 @@ SOURCES += \ RealtimeWindow.cpp \ RealtimePlot.cpp \ RideCalendar.cpp \ + RideEditor.cpp \ RideFile.cpp \ + RideFileCommand.cpp \ + RideFileTableModel.cpp \ RideImportWizard.cpp \ RideItem.cpp \ RideMetadata.cpp \