mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-15 00:49:55 +00:00
Ride editor and tools
A new tab 'Editor' for manually editing ride file data points and associated menu options under 'Tools' for fixing spikes, gaps, GPS errors and adjusting torque values. A revert to saved ride option is also included to 'undo' all changes. The ride editor supports undo/redo as well as cut and paste and "paste special" (to append points or swap columns/overwrite selected data series). The editor also supports search and will automatically highlight anomalous data. When a file is saved, the changes are recorded in a new metadata special field called "Change History" which can be added as a Textbox in the metadata config. The data processors can be run manually or automatically when a ride is opened - these are configured on the ride data tab in the config pane. Significant changes have been introduced in the codebase, the most significant of which are; a RideFileCommand class for modifying ride data has been introduced (as a member of RideFile) and the RideItem class is now a QObject as well as QTreeWidgetItem to enable signalling. The Ride Editor uses a RideFileTableModel that can be re-used in other parts of the code. LTMoutliers class has been introduced in support of anomaly detection in the editor (which highlights anomalies with a wiggly red line). Fixes #103.
This commit is contained in:
380
src/RideFileTableModel.cpp
Normal file
380
src/RideFileTableModel.cpp
Normal file
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "RideFileTableModel.h"
|
||||
|
||||
RideFileTableModel::RideFileTableModel(RideFile *ride) : ride(ride)
|
||||
{
|
||||
setRide(ride);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setRide(RideFile *newride)
|
||||
{
|
||||
// QPointer helps us check if the current ride has been deleted before trying to disconnect
|
||||
static QPointer<RideFileCommand> connection = NULL;
|
||||
if (connection) {
|
||||
disconnect(connection, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*)));
|
||||
disconnect(connection, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*)));
|
||||
}
|
||||
|
||||
ride = newride;
|
||||
tooltips.clear(); // remove the tooltips -- rideEditor will set them (this is fugly, but efficient)
|
||||
|
||||
if (ride) {
|
||||
|
||||
// set the headings to reflect the data that is present
|
||||
setHeadings();
|
||||
|
||||
// Trap commands
|
||||
connection = ride->command;
|
||||
connect(ride->command, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*)));
|
||||
connect(ride->command, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*)));
|
||||
|
||||
// refresh
|
||||
emit layoutChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setHeadings(RideFile::SeriesType series)
|
||||
{
|
||||
headings_.clear();
|
||||
headingsType.clear();
|
||||
|
||||
// set the headings array
|
||||
if (series == RideFile::secs || ride->areDataPresent()->secs) {
|
||||
headings_ << tr("Time");
|
||||
headingsType << RideFile::secs;
|
||||
}
|
||||
if (series == RideFile::km || ride->areDataPresent()->km) {
|
||||
headings_ << tr("Distance");
|
||||
headingsType << RideFile::km;
|
||||
}
|
||||
if (series == RideFile::watts || ride->areDataPresent()->watts) {
|
||||
headings_ << tr("Power");
|
||||
headingsType << RideFile::watts;
|
||||
}
|
||||
if (series == RideFile::nm || ride->areDataPresent()->nm) {
|
||||
headings_ << tr("Torque");
|
||||
headingsType << RideFile::nm;
|
||||
}
|
||||
if (series == RideFile::cad || ride->areDataPresent()->cad) {
|
||||
headings_ << tr("Cadence");
|
||||
headingsType << RideFile::cad;
|
||||
}
|
||||
if (series == RideFile::hr || ride->areDataPresent()->hr) {
|
||||
headings_ << tr("Heartrate");
|
||||
headingsType << RideFile::hr;
|
||||
}
|
||||
if (series == RideFile::kph || ride->areDataPresent()->kph) {
|
||||
headings_ << tr("Speed");
|
||||
headingsType << RideFile::kph;
|
||||
}
|
||||
if (series == RideFile::alt || ride->areDataPresent()->alt) {
|
||||
headings_ << tr("Altitude");
|
||||
headingsType << RideFile::alt;
|
||||
}
|
||||
if (series == RideFile::lat || ride->areDataPresent()->lat) {
|
||||
headings_ << tr("Latitude");
|
||||
headingsType << RideFile::lat;
|
||||
}
|
||||
if (series == RideFile::lon || ride->areDataPresent()->lon) {
|
||||
headings_ << tr("Longitude");
|
||||
headingsType << RideFile::lon;
|
||||
}
|
||||
if (series == RideFile::headwind || ride->areDataPresent()->headwind) {
|
||||
headings_ << tr("Headwind");
|
||||
headingsType << RideFile::headwind;
|
||||
}
|
||||
if (series == RideFile::interval || ride->areDataPresent()->interval) {
|
||||
headings_ << tr("Interval");
|
||||
headingsType << RideFile::interval;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Qt::ItemFlags
|
||||
RideFileTableModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::ItemIsEditable;
|
||||
else
|
||||
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable |
|
||||
Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
QVariant
|
||||
RideFileTableModel::data(const QModelIndex & index, int role) const
|
||||
{
|
||||
if (role == Qt::ToolTipRole) return toolTip(index.row(), columnType(index.column()));
|
||||
|
||||
if (index.row() >= ride->dataPoints().count() || index.column() >= headings_.count())
|
||||
return QVariant();
|
||||
else
|
||||
return ride->getPointValue(index.row(), headingsType[index.column()]);
|
||||
}
|
||||
|
||||
QVariant
|
||||
RideFileTableModel::headerData(int section, Qt::Orientation orient, int role) const
|
||||
{
|
||||
if (role != Qt::DisplayRole) return QVariant();
|
||||
|
||||
if (orient == Qt::Horizontal) {
|
||||
if (section >= headings_.count())
|
||||
return QVariant();
|
||||
else {
|
||||
return headings_[section];
|
||||
}
|
||||
} else {
|
||||
return QString("%1").arg(section+1);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
RideFileTableModel::rowCount(const QModelIndex &) const
|
||||
{
|
||||
if (ride) return ride->dataPoints().count();
|
||||
else return 0;
|
||||
}
|
||||
|
||||
int
|
||||
RideFileTableModel::columnCount(const QModelIndex &) const
|
||||
{
|
||||
if (ride) return headingsType.count();
|
||||
else return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::setData(const QModelIndex & index, const QVariant &value, int role)
|
||||
{
|
||||
if (index.row() >= ride->dataPoints().count() || index.column() >= headings_.count())
|
||||
return false;
|
||||
else if (role == Qt::EditRole) {
|
||||
ride->command->setPointValue(index.row(), headingsType[index.column()], value.toDouble());
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::setHeaderData(int section, Qt::Orientation , const QVariant & value, int)
|
||||
{
|
||||
if (section >= headings_.count()) return false;
|
||||
else {
|
||||
headings_[section] = value.toString();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertRow(int row, const QModelIndex &parent)
|
||||
{
|
||||
return insertRows(row, 1, parent);
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertRows(int row, int count, const QModelIndex &)
|
||||
{
|
||||
if (row >= ride->dataPoints().count()) return false;
|
||||
else {
|
||||
while (count--) {
|
||||
struct RideFilePoint *p = new RideFilePoint;
|
||||
ride->command->insertPoint(row, p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::appendRows(QVector<RideFilePoint>newRows)
|
||||
{
|
||||
ride->command->appendPoints(newRows);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::removeRows(int row, int count, const QModelIndex &)
|
||||
{
|
||||
if ((row + count) > ride->dataPoints().count()) return false;
|
||||
ride->command->deletePoints(row, count);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertColumn(RideFile::SeriesType series)
|
||||
{
|
||||
if (headingsType.contains(series)) return false; // already there
|
||||
|
||||
ride->command->setDataPresent(series, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::insertColumns(int, int, const QModelIndex &)
|
||||
{
|
||||
// WE DON'T SUPPORT THIS
|
||||
// use insertColumn(RideFile::SeriesType) instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::removeColumn(RideFile::SeriesType series)
|
||||
{
|
||||
if (headingsType.contains(series)) {
|
||||
ride->command->setDataPresent(series, false);
|
||||
return true;
|
||||
} else
|
||||
return false; // its not there
|
||||
}
|
||||
|
||||
bool
|
||||
RideFileTableModel::removeColumns (int , int , const QModelIndex &)
|
||||
{
|
||||
// WE DON'T SUPPORT THIS
|
||||
// use removeColumn(RideFile::SeriesType) instead.
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setValue(int row, int column, double value)
|
||||
{
|
||||
ride->command->setPointValue(row, headingsType[column], value);
|
||||
}
|
||||
|
||||
double
|
||||
RideFileTableModel::getValue(int row, int column)
|
||||
{
|
||||
return ride->getPointValue(row, headingsType[column]);
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::forceRedraw()
|
||||
{
|
||||
// tell the view to redraw everything
|
||||
dataChanged(createIndex(0,0), createIndex(headingsType.count(), ride->dataPoints().count()));
|
||||
}
|
||||
|
||||
//
|
||||
// RideCommand has made changes...
|
||||
//
|
||||
void
|
||||
RideFileTableModel::beginCommand(bool undo, RideCommand *cmd)
|
||||
{
|
||||
switch (cmd->type) {
|
||||
|
||||
case RideCommand::SetPointValue:
|
||||
break;
|
||||
|
||||
case RideCommand::InsertPoint:
|
||||
{
|
||||
InsertPointCommand *dp = (InsertPointCommand *)cmd;
|
||||
if (!undo) beginInsertRows(QModelIndex(), dp->row, dp->row);
|
||||
else beginRemoveRows(QModelIndex(), dp->row, dp->row);
|
||||
break;
|
||||
}
|
||||
|
||||
case RideCommand::DeletePoint:
|
||||
{
|
||||
DeletePointCommand *dp = (DeletePointCommand *)cmd;
|
||||
if (undo) beginInsertRows(QModelIndex(), dp->row, dp->row);
|
||||
else beginRemoveRows(QModelIndex(), dp->row, dp->row);
|
||||
break;
|
||||
}
|
||||
|
||||
case RideCommand::DeletePoints:
|
||||
{
|
||||
DeletePointsCommand *ds = (DeletePointsCommand *)cmd;
|
||||
if (undo) beginInsertRows(QModelIndex(), ds->row, ds->row + ds->count - 1);
|
||||
else beginRemoveRows(QModelIndex(), ds->row, ds->row + ds->count - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case RideCommand::AppendPoints:
|
||||
{
|
||||
AppendPointsCommand *ap = (AppendPointsCommand *)cmd;
|
||||
if (!undo) beginInsertRows(QModelIndex(), ap->row, ap->row + ap->count - 1);
|
||||
else beginRemoveRows(QModelIndex(), ap->row, ap->row + ap->count - 1);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::endCommand(bool undo, RideCommand *cmd)
|
||||
{
|
||||
switch (cmd->type) {
|
||||
|
||||
case RideCommand::SetPointValue:
|
||||
{
|
||||
SetPointValueCommand *spv = (SetPointValueCommand*)cmd;
|
||||
QModelIndex cell(index(spv->row,headingsType.indexOf(spv->series)));
|
||||
dataChanged(cell, cell);
|
||||
break;
|
||||
}
|
||||
case RideCommand::InsertPoint:
|
||||
if (!undo) endInsertRows();
|
||||
else endRemoveRows();
|
||||
break;
|
||||
|
||||
case RideCommand::DeletePoint:
|
||||
case RideCommand::DeletePoints:
|
||||
if (undo) endInsertRows();
|
||||
else endRemoveRows();
|
||||
break;
|
||||
|
||||
case RideCommand::AppendPoints:
|
||||
if (undo) endRemoveRows();
|
||||
else endInsertRows();
|
||||
break;
|
||||
|
||||
case RideCommand::SetDataPresent:
|
||||
setHeadings();
|
||||
emit layoutChanged();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltips are kept in a QMap, since they SHOULD be sparse
|
||||
static QString xsstring(int x, RideFile::SeriesType series)
|
||||
{
|
||||
return QString("%1:%2").arg((int)x).arg(static_cast<int>(series));
|
||||
}
|
||||
|
||||
void
|
||||
RideFileTableModel::setToolTip(int row, RideFile::SeriesType series, QString text)
|
||||
{
|
||||
QString key = xsstring(row, series);
|
||||
|
||||
// if text is blank we are removing it
|
||||
if (text == "") tooltips.remove(key);
|
||||
|
||||
// if text is non-blank we are changing it
|
||||
if (text != "") tooltips.insert(key, text);
|
||||
}
|
||||
|
||||
QString
|
||||
RideFileTableModel::toolTip(int row, RideFile::SeriesType series) const
|
||||
{
|
||||
QString key = xsstring(row, series);
|
||||
return tooltips.value(key, "");
|
||||
}
|
||||
Reference in New Issue
Block a user