mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
Will keep all intervals that start within a split but will truncate for intervals that are longer than the split so we keep any 'Lap markers'.
837 lines
29 KiB
C++
837 lines
29 KiB
C++
/*
|
|
* Copyright (c) 2011 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 "SplitActivityWizard.h"
|
|
|
|
// Minimum gap in recording to find a natural break to split
|
|
static const double defaultMinimumGap = 15; // 15 minutes
|
|
|
|
// Minimum size of segment to identify as a new activity
|
|
static const double defaultMinimumSegmentSize = 20; // 20 minutes
|
|
|
|
// Main wizard
|
|
SplitActivityWizard::SplitActivityWizard(MainWindow *main) : QWizard(main), main(main)
|
|
{
|
|
// delete when done
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
// Minimum 600x500 for when selecting intervals
|
|
setMinimumHeight(500);
|
|
setMinimumWidth(600);
|
|
|
|
// title
|
|
setWindowTitle(tr("Split Activity"));
|
|
|
|
// set ride - unconst since we will wipe it away eventually
|
|
rideItem = const_cast<RideItem*>(main->currentRideItem());
|
|
|
|
// Set sensible defaults
|
|
keepOriginal = false;
|
|
minimumGap = defaultMinimumGap;
|
|
minimumSegmentSize = defaultMinimumSegmentSize;
|
|
usedMinimumSegmentSize = usedMinimumGap = -1;
|
|
|
|
// set initial intervals list, will be adjusted
|
|
// if the user modifies the default paramters
|
|
intervals = new QTreeWidget;
|
|
intervals->headerItem()->setText(0, tr(""));
|
|
intervals->headerItem()->setText(1, tr("Start"));
|
|
intervals->headerItem()->setText(2, tr(""));
|
|
intervals->headerItem()->setText(3, tr("Stop"));
|
|
intervals->headerItem()->setText(4, tr("Duration"));
|
|
intervals->headerItem()->setText(5, tr("Distance"));
|
|
intervals->headerItem()->setText(6, tr("Interval Name"));
|
|
intervals->setColumnCount(7);
|
|
intervals->setColumnWidth(0,30);
|
|
intervals->setColumnWidth(1,80);
|
|
intervals->setColumnWidth(2,30);
|
|
intervals->setColumnWidth(3,80);
|
|
intervals->setColumnWidth(4,80);
|
|
intervals->setColumnWidth(5,80);
|
|
intervals->setSelectionMode(QAbstractItemView::NoSelection);
|
|
intervals->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit
|
|
intervals->setUniformRowHeights(true);
|
|
intervals->setIndentation(0);
|
|
|
|
files = new QTreeWidget;
|
|
files->headerItem()->setText(0, tr("Filename"));
|
|
files->headerItem()->setText(1, tr("Date"));
|
|
files->headerItem()->setText(2, tr("Time"));
|
|
files->headerItem()->setText(3, tr("Duration"));
|
|
files->headerItem()->setText(4, tr("Distance"));
|
|
files->headerItem()->setText(5, tr("Action"));
|
|
files->setColumnCount(6);
|
|
files->setColumnWidth(0, 190); // filename
|
|
files->setColumnWidth(1, 95); // date
|
|
files->setColumnWidth(2, 90); // time
|
|
files->setColumnWidth(3, 75); // duration
|
|
files->setColumnWidth(4, 75); // distance
|
|
files->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
files->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit
|
|
files->setUniformRowHeights(true);
|
|
files->setIndentation(0);
|
|
|
|
// just the hr and power as a plot
|
|
smallPlot = new SmallPlot(this);
|
|
smallPlot->setFixedHeight(100);
|
|
smallPlot->setData(rideItem);
|
|
|
|
bg = new SplitBackground(this);
|
|
bg->attach(smallPlot);
|
|
|
|
// 5 step process, although Conflict may be skipped
|
|
addPage(new SplitWelcome(this));
|
|
addPage(new SplitKeep(this));
|
|
addPage(new SplitParameters(this));
|
|
addPage(new SplitSelect(this));
|
|
addPage(new SplitConfirm(this));
|
|
|
|
done = false;
|
|
}
|
|
|
|
void
|
|
SplitActivityWizard::setIntervalsList(SplitSelect *selector)
|
|
{
|
|
// didn't change so no need to rebuild
|
|
if (usedMinimumGap == minimumGap && usedMinimumSegmentSize == minimumSegmentSize) return;
|
|
|
|
// clear the table
|
|
intervals->clear();
|
|
|
|
// convert to seconds
|
|
int minimumGap = this->minimumGap * 60;
|
|
int minimumSegmentSize = this->minimumSegmentSize * 60;
|
|
|
|
// remember the last ones we used
|
|
usedMinimumSegmentSize = this->minimumSegmentSize;
|
|
usedMinimumGap = this->minimumGap;
|
|
|
|
// find segments where gap is greater than minimumGap
|
|
// and segment size is > minimumSize. If a segment is shorter
|
|
// than minimumSize then ignore it (i.e. treat is as part of the gap)
|
|
QList<RideFileInterval> segments;
|
|
double segmentStart = 0;
|
|
double segmentEnd = 0;
|
|
|
|
double lastSecs = 0;
|
|
bool first = true;
|
|
|
|
int counter = 0;
|
|
foreach (RideFilePoint *p, rideItem->ride()->dataPoints()) {
|
|
|
|
if (first == true) {
|
|
|
|
segmentStart = segmentEnd = lastSecs = p->secs;
|
|
first = false;
|
|
|
|
} else {
|
|
|
|
if ((p->secs - segmentEnd) >= minimumGap) {
|
|
|
|
if ((segmentEnd-segmentStart) >= minimumSegmentSize) {
|
|
|
|
// we have a candidate
|
|
segments.append(RideFileInterval(segmentStart, segmentEnd,
|
|
QString("Activity Segment #%1").arg(++counter)));
|
|
|
|
}
|
|
segmentEnd = segmentStart = p->secs;
|
|
|
|
} else {
|
|
|
|
// keep accumulating
|
|
segmentEnd = p->secs;
|
|
}
|
|
}
|
|
lastSecs = p->secs;
|
|
}
|
|
|
|
// we got to the end, is there a segment here?
|
|
if ((segmentEnd-segmentStart) >= minimumSegmentSize) {
|
|
|
|
// we have a candidate
|
|
segments.append(RideFileInterval(segmentStart, segmentEnd,
|
|
QString("Activity Segment #%1").arg(++counter)));
|
|
|
|
}
|
|
|
|
|
|
// now look at the segments and add any gaps in recording
|
|
// because these MUST be honoured (i.e. user cannot unmark
|
|
// these points, otherwise gaps in recording are retained
|
|
// in the resulting file
|
|
foreach(RideFileInterval *p, gaps) { delete p; } gaps.clear(); // wip old ones
|
|
|
|
double lastsecs = rideItem->ride()->dataPoints().first()->secs;
|
|
int gapnum = 0;
|
|
foreach(RideFileInterval activity, segments) {
|
|
if (activity.start > lastsecs) {
|
|
// we have a gap
|
|
gapnum++;
|
|
|
|
// add to gap list
|
|
RideFileInterval *gap = new RideFileInterval(lastsecs,
|
|
activity.start,
|
|
QString("Gap in recording #%1").arg(gapnum));
|
|
gaps.append(gap);
|
|
|
|
// add to interval list
|
|
segments.append(RideFileInterval(*gap));
|
|
}
|
|
lastsecs = activity.stop;
|
|
}
|
|
if (lastsecs < rideItem->ride()->dataPoints().last()->secs) {
|
|
// gap at the end
|
|
gapnum++;
|
|
|
|
// add to gap list
|
|
RideFileInterval *gap = new RideFileInterval(lastsecs,
|
|
rideItem->ride()->dataPoints().last()->secs,
|
|
QString("Gap in recording #%1").arg(gapnum));
|
|
gaps.append(gap);
|
|
|
|
// add to interval list
|
|
segments.append(RideFileInterval(*gap));
|
|
}
|
|
|
|
// first entry in list should always be entire file
|
|
// so we can mark the start and stop for splitting
|
|
segments.insert(0, RideFileInterval(rideItem->ride()->dataPoints().first()->secs,
|
|
rideItem->ride()->dataPoints().last()->secs,
|
|
"Entire Activity"));
|
|
|
|
// now fold in the ride intervals
|
|
segments.append(rideItem->ride()->intervals());
|
|
|
|
// now lets sort the segments in start order
|
|
qSort(segments.begin(), segments.end());
|
|
|
|
// first just add all the current ride intervals
|
|
counter = 0;
|
|
QChar zero = QLatin1Char('0');
|
|
foreach (RideFileInterval interval, segments) {
|
|
|
|
// DO NOT skip intervals that are too short
|
|
//if (interval.stop - interval.start < minimumSegmentSize) continue;
|
|
|
|
QTreeWidgetItem *add = new QTreeWidgetItem(intervals->invisibleRootItem());
|
|
add->setFlags(add->flags() | Qt::ItemIsEditable);
|
|
|
|
// we set these intervals as checked by default
|
|
bool checkit = (interval.name.startsWith("Gap in recording") ||
|
|
interval.name == "Entire Activity");
|
|
|
|
// disable checkbox editing (i.e. mandatory split) at gaps in recording
|
|
bool disableit = (interval.name.startsWith("Gap in recording"));
|
|
|
|
// selector start
|
|
QCheckBox *checkBox = new QCheckBox("", this);
|
|
checkBox->setChecked(checkit);
|
|
checkBox->setEnabled(!disableit);
|
|
intervals->setItemWidget(add, 0, checkBox);
|
|
|
|
connect(checkBox, SIGNAL(stateChanged(int)), selector, SLOT(refreshMarkers()));
|
|
|
|
// interval start
|
|
int secs = interval.start;
|
|
add->setText(1, QString("%1:%2:%3")
|
|
.arg(secs/3600,2,10,zero)
|
|
.arg(secs%3600/60,2,10,zero)
|
|
.arg(secs%60,2,10,zero));
|
|
|
|
// selector stop
|
|
checkBox = new QCheckBox("", this);
|
|
checkBox->setChecked(checkit);
|
|
checkBox->setEnabled(!disableit);
|
|
intervals->setItemWidget(add, 2, checkBox);
|
|
|
|
connect(checkBox, SIGNAL(stateChanged(int)), selector, SLOT(refreshMarkers()));
|
|
|
|
// interval start
|
|
secs = interval.stop;
|
|
add->setText(3, QString("%1:%2:%3")
|
|
.arg(secs/3600,2,10,zero)
|
|
.arg(secs%3600/60,2,10,zero)
|
|
.arg(secs%60,2,10,zero));
|
|
|
|
// interval duration
|
|
secs = interval.stop - interval.start;
|
|
add->setText(4, QString("%1:%2:%3")
|
|
.arg(secs/3600,2,10,zero)
|
|
.arg(secs%3600/60,2,10,zero)
|
|
.arg(secs%60,2,10,zero));
|
|
|
|
// interval distance
|
|
double distance = rideItem->ride()->timeToDistance(interval.stop) -
|
|
rideItem->ride()->timeToDistance(interval.start);
|
|
add->setText(5, QString("%1 %2")
|
|
.arg(distance * (main->useMetricUnits ? 1 : MILES_PER_KM), 0, 'f', 2)
|
|
.arg(main->useMetricUnits ? "km" : "mi"));
|
|
|
|
// interval name
|
|
add->setText(6, interval.name);
|
|
|
|
// hiddden columns with dataPoint from/to
|
|
add->setText(7, QString("%1").arg(rideItem->ride()->timeIndex(interval.start)));
|
|
add->setText(8, QString("%1").arg(rideItem->ride()->timeIndex(interval.stop)));
|
|
|
|
counter++;
|
|
}
|
|
smallPlot->replot();
|
|
}
|
|
|
|
void
|
|
SplitActivityWizard::setFilesList()
|
|
{
|
|
// clear the table
|
|
files->clear();
|
|
|
|
// fold in current ride -- if we are removing
|
|
if (keepOriginal == false) {
|
|
// we will wipe the original file
|
|
QTreeWidgetItem *add = new QTreeWidgetItem(files->invisibleRootItem());
|
|
add->setFlags(add->flags() | Qt::ItemIsEditable);
|
|
|
|
add->setText(0, rideItem->fileName);
|
|
add->setText(1, rideItem->ride()->startTime().toString(tr("dd MMM yyyy")));
|
|
add->setText(2, rideItem->ride()->startTime().toString(tr("hh:mm:ss ap")));
|
|
|
|
// get duration and distance, yuk, dup of code below, and from RideImportWizard
|
|
int secs=0;
|
|
double km=0;
|
|
if (!rideItem->ride()->dataPoints().isEmpty() && rideItem->ride()->dataPoints().last() != NULL) {
|
|
if (!secs) secs = rideItem->ride()->dataPoints().last()->secs;
|
|
if (!km) km = rideItem->ride()->dataPoints().last()->km;
|
|
}
|
|
|
|
// set duration
|
|
QChar zero = QLatin1Char ( '0' );
|
|
QString time = QString("%1:%2:%3").arg(secs/3600,2,10,zero)
|
|
.arg(secs%3600/60,2,10,zero)
|
|
.arg(secs%60,2,10,zero);
|
|
add->setText(3, time);
|
|
|
|
// set distance
|
|
QString dist = main->useMetricUnits
|
|
? QString ("%1 km").arg(km, 0, 'f', 1)
|
|
: QString ("%1 mi").arg(km * MILES_PER_KM, 0, 'f', 1);
|
|
add->setText(4, dist);
|
|
|
|
// interval action
|
|
add->setText(5, "Remove");
|
|
}
|
|
|
|
// create a row for each file and action
|
|
QChar zero = QLatin1Char('0');
|
|
foreach (RideFile *ride, activities) {
|
|
|
|
QTreeWidgetItem *add = new QTreeWidgetItem(files->invisibleRootItem());
|
|
add->setFlags(add->flags() | Qt::ItemIsEditable);
|
|
|
|
QString filename = QString ("%1_%2_%3_%4_%5_%6.json")
|
|
.arg(ride->startTime().date().year(), 4, 10, zero)
|
|
.arg(ride->startTime().date().month(), 2, 10, zero)
|
|
.arg(ride->startTime().date().day(), 2, 10, zero)
|
|
.arg(ride->startTime().time().hour(), 2, 10, zero)
|
|
.arg(ride->startTime().time().minute(), 2, 10, zero)
|
|
.arg(ride->startTime().time().second(), 2, 10, zero);
|
|
|
|
// filename
|
|
add->setText(0, filename);
|
|
|
|
// date and time
|
|
add->setText(1, ride->startTime().toString(tr("dd MMM yyyy")));
|
|
add->setText(2, ride->startTime().toString(tr("hh:mm:ss ap")));
|
|
|
|
// get duration and distance
|
|
int secs=0;
|
|
double km=0;
|
|
if (!ride->dataPoints().isEmpty() && ride->dataPoints().last() != NULL) {
|
|
if (!secs) secs = ride->dataPoints().last()->secs;
|
|
if (!km) km = ride->dataPoints().last()->km;
|
|
}
|
|
|
|
// set duration
|
|
QChar zero = QLatin1Char ( '0' );
|
|
QString time = QString("%1:%2:%3").arg(secs/3600,2,10,zero)
|
|
.arg(secs%3600/60,2,10,zero)
|
|
.arg(secs%60,2,10,zero);
|
|
add->setText(3, time);
|
|
|
|
// set distance
|
|
QString dist = main->useMetricUnits
|
|
? QString ("%1 km").arg(km, 0, 'f', 1)
|
|
: QString ("%1 mi").arg(km * MILES_PER_KM, 0, 'f', 1);
|
|
add->setText(4, dist);
|
|
|
|
// interval action
|
|
add->setText(5, "Create");
|
|
}
|
|
|
|
// tidy up column widths
|
|
files->resizeColumnToContents(5);
|
|
}
|
|
|
|
// if the file in question has a backup file already then
|
|
// return the filename (without the path), otherwise return
|
|
// an empty string
|
|
QString
|
|
SplitActivityWizard::hasBackup(QString filename)
|
|
{
|
|
QString backupFilename = main->home.absolutePath() + "/" + filename + ".bak";
|
|
|
|
if (QFile(backupFilename).exists()) {
|
|
|
|
return QString(filename + ".bak");
|
|
|
|
} else {
|
|
|
|
return "";
|
|
}
|
|
}
|
|
|
|
QStringList
|
|
SplitActivityWizard::conflicts(QDateTime datetime)
|
|
{
|
|
QStringList returning;
|
|
|
|
// Check if an existing ride has the same starttime
|
|
QChar zero = QLatin1Char('0');
|
|
QString targetnosuffix = QString ("%1_%2_%3_%4_%5_%6")
|
|
.arg(datetime.date().year(), 4, 10, zero)
|
|
.arg(datetime.date().month(), 2, 10, zero)
|
|
.arg(datetime.date().day(), 2, 10, zero)
|
|
.arg(datetime.time().hour(), 2, 10, zero)
|
|
.arg(datetime.time().minute(), 2, 10, zero)
|
|
.arg(datetime.time().second(), 2, 10, zero);
|
|
|
|
// now make a regexp for all know ride types
|
|
foreach(QString suffix, RideFileFactory::instance().suffixes()) {
|
|
|
|
QString conflict = main->home.absolutePath() + "/" + targetnosuffix + "." + suffix;
|
|
if (QFile(conflict).exists()) returning << conflict;
|
|
}
|
|
return returning;
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Wizard Pages
|
|
*--------------------------------------------------------------------*/
|
|
|
|
// welcome
|
|
SplitWelcome::SplitWelcome(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent)
|
|
{
|
|
setTitle(tr("Split Activity"));
|
|
setSubTitle(tr("Lets get started"));
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout;
|
|
setLayout(layout);
|
|
|
|
QLabel *label = new QLabel("This wizard will help you split the current activity "
|
|
"into multiple activities\n\n"
|
|
"The wizard will identify segments of uninterrupted "
|
|
"activity and allow you to select which ones to "
|
|
"save as new activities. You will also be able to "
|
|
"select any currently defined intervals too.\n\n"
|
|
"If the newly created activity clashes with an existing "
|
|
"activity (same date and time) then the wizard will adjust "
|
|
"the start time by one or more seconds to avoid losing or "
|
|
"overwriting any existing activities.");
|
|
label->setWordWrap(true);
|
|
|
|
layout->addWidget(label);
|
|
layout->addStretch();
|
|
}
|
|
|
|
// Keep original?
|
|
SplitKeep::SplitKeep(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent)
|
|
{
|
|
setTitle(tr("Keep original"));
|
|
setSubTitle(tr("Do you want to keep the original activity?"));
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout;
|
|
setLayout(layout);
|
|
|
|
QLabel *label = new QLabel("If you want to keep the current activity then you "
|
|
"should ensure you have clicked on the \"Keep original "
|
|
"activity\" check box below.\n\n"
|
|
"If you do not choose to keep the original activity "
|
|
"it will be backed up before removing it from the "
|
|
"activity history.\n\n");
|
|
label->setWordWrap(true);
|
|
|
|
keepOriginal = new QCheckBox("Keep original activity", this);
|
|
keepOriginal->setChecked(wizard->keepOriginal);
|
|
|
|
warning = new QLabel(this);
|
|
warning->setWordWrap(true);
|
|
QFont font;
|
|
font.setWeight(QFont::Bold);
|
|
warning->setFont(font);
|
|
|
|
setWarning();
|
|
|
|
layout->addWidget(label);
|
|
layout->addWidget(keepOriginal);
|
|
layout->addStretch();
|
|
layout->addWidget(warning);
|
|
|
|
connect(keepOriginal, SIGNAL(stateChanged(int)), this, SLOT(keepOriginalChanged()));
|
|
}
|
|
|
|
// paramters
|
|
SplitParameters::SplitParameters(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent)
|
|
{
|
|
setTitle(tr("Split Parameters"));
|
|
setSubTitle(tr("Configure how segments are found"));
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout;
|
|
setLayout(layout);
|
|
|
|
QLabel *label = new QLabel("This wizard will find segments of activity to save "
|
|
"by looking for gaps in recording. \n\n"
|
|
"You can define the minimum length, in time, a gap "
|
|
"in recording should be in order to mark the end of "
|
|
"one segment and the beginning of another.\n\n"
|
|
"In addition, you can set a minimum segment size. "
|
|
"Any segment smaller than this limit will be ignored.\n\n");
|
|
label->setWordWrap(true);
|
|
|
|
layout->addWidget(label);
|
|
|
|
QGridLayout *grid = new QGridLayout;
|
|
QLabel *minGap = new QLabel("Minimum Gap (minutes)", this);
|
|
QLabel *minSize = new QLabel("Minimum Segment Size (minutes)", this);
|
|
|
|
minimumGap = new QDoubleSpinBox(this);
|
|
minimumGap->setDecimals(0);
|
|
minimumGap->setSingleStep(1.0);
|
|
minimumGap->setValue(wizard->minimumGap);
|
|
|
|
minimumSegmentSize = new QDoubleSpinBox(this);
|
|
minimumSegmentSize->setDecimals(0);
|
|
minimumSegmentSize->setSingleStep(1.0);
|
|
minimumSegmentSize->setValue(wizard->minimumSegmentSize);
|
|
|
|
grid->addWidget(minGap,0,0);
|
|
grid->addWidget(minimumGap,0,1,Qt::AlignLeft);
|
|
grid->addWidget(minSize,1,0);
|
|
grid->addWidget(minimumSegmentSize,1,1,Qt::AlignLeft);
|
|
|
|
layout->addLayout(grid);
|
|
layout->addStretch();
|
|
|
|
connect (minimumGap, SIGNAL(valueChanged(double)), this, SLOT(valueChanged()));
|
|
connect (minimumSegmentSize, SIGNAL(valueChanged(double)), this, SLOT(valueChanged()));
|
|
}
|
|
|
|
void
|
|
SplitParameters::valueChanged()
|
|
{
|
|
wizard->minimumGap = minimumGap->value();
|
|
wizard->minimumSegmentSize = minimumSegmentSize->value();
|
|
}
|
|
|
|
void
|
|
SplitKeep::keepOriginalChanged()
|
|
{
|
|
wizard->keepOriginal = keepOriginal->isChecked();
|
|
setWarning();
|
|
}
|
|
|
|
void
|
|
SplitKeep::setWarning()
|
|
{
|
|
if (!keepOriginal->isChecked()) {
|
|
|
|
if (wizard->hasBackup(wizard->rideItem->fileName) != "") {
|
|
|
|
warning->setText("WARNING: The current ride will be backed up and "
|
|
"removed, but a backup already exists. The existing "
|
|
"backup will therefore be overwritten.");
|
|
return;
|
|
}
|
|
}
|
|
warning->setText("");
|
|
}
|
|
|
|
// Select
|
|
SplitSelect::SplitSelect(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent)
|
|
{
|
|
setTitle(tr("Select Split Markers"));
|
|
setSubTitle(tr("Activity will be split between marker points selected"));
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout;
|
|
setLayout(layout);
|
|
|
|
layout->addWidget(wizard->smallPlot);
|
|
layout->addWidget(wizard->intervals);
|
|
}
|
|
void
|
|
SplitSelect::initializePage()
|
|
{
|
|
wizard->setIntervalsList(this);
|
|
refreshMarkers();
|
|
}
|
|
|
|
void
|
|
SplitSelect::refreshMarkers()
|
|
{
|
|
// update the markers on the plot
|
|
foreach (QwtPlotMarker *m, wizard->markers) {
|
|
// remove from the plot and delete
|
|
m->detach();
|
|
delete m;
|
|
}
|
|
wizard->markers.clear();
|
|
wizard->marks.clear(); // dataPoint indexes
|
|
|
|
// now refresh them
|
|
for(int i=0; i<wizard->intervals->invisibleRootItem()->childCount(); i++) {
|
|
|
|
QTreeWidgetItem *current = wizard->intervals->invisibleRootItem()->child(i);
|
|
|
|
// add marker for start?
|
|
if (static_cast<QCheckBox*>(wizard->intervals->itemWidget(current,0))->isChecked()) {
|
|
|
|
long index = current->text(7).toInt();
|
|
double point = wizard->rideItem->ride()->dataPoints().at(index)->secs;
|
|
|
|
wizard->marks.append(index);
|
|
QwtPlotMarker *add = new QwtPlotMarker;
|
|
wizard->markers.append(add);
|
|
|
|
// vertical line will do for now
|
|
add->setLineStyle(QwtPlotMarker::VLine);
|
|
add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
|
|
add->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
|
|
add->setValue(point / 60.0, 0.0);
|
|
add->attach(wizard->smallPlot);
|
|
}
|
|
|
|
// add marker for stop?
|
|
if (static_cast<QCheckBox*>(wizard->intervals->itemWidget(current,2))->isChecked()) {
|
|
|
|
long index = current->text(8).toInt();
|
|
double point = wizard->rideItem->ride()->dataPoints().at(index)->secs;
|
|
|
|
wizard->marks.append(index);
|
|
QwtPlotMarker *add = new QwtPlotMarker;
|
|
wizard->markers.append(add);
|
|
|
|
// vertical line will do for now
|
|
add->setLineStyle(QwtPlotMarker::VLine);
|
|
add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
|
|
add->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
|
|
add->setValue(point / 60.0, 0.0);
|
|
add->attach(wizard->smallPlot);
|
|
}
|
|
}
|
|
wizard->smallPlot->replot();
|
|
}
|
|
|
|
// Confirm
|
|
SplitConfirm::SplitConfirm(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent)
|
|
{
|
|
setTitle(tr("Confirm"));
|
|
setSubTitle(tr("Split Activity cannot be undone"));
|
|
|
|
setCommitPage(true);
|
|
setButtonText(QWizard::CommitButton, "Confirm");
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout;
|
|
setLayout(layout);
|
|
|
|
layout->addWidget(wizard->files);
|
|
}
|
|
|
|
// create an array of rides
|
|
void
|
|
SplitConfirm::initializePage()
|
|
{
|
|
// clear the current array
|
|
foreach(RideFile *ride, wizard->activities) {
|
|
delete ride;
|
|
}
|
|
wizard->activities.clear();
|
|
|
|
// create a sorted list of markers, since we
|
|
// may have duplicates and the sequence is
|
|
// not guaranteed to be ordered
|
|
QList<long> points;
|
|
foreach(long mark, wizard->marks) points.append(mark); // marks are indexes to ensure absolute accuracy
|
|
qSort(points.begin(), points.end());
|
|
|
|
// Create a new activity for each marked segment
|
|
long lastmark = -1;
|
|
foreach(long mark, points) {
|
|
|
|
if (mark == lastmark) continue;
|
|
|
|
if (lastmark != -1) {
|
|
|
|
// ignore gaps!
|
|
if (!wizard->bg->isGap(wizard->rideItem->ride()->dataPoints().at(lastmark)->secs/60.0,
|
|
wizard->rideItem->ride()->dataPoints().at(mark)->secs/60.0)) {
|
|
|
|
RideFile *add = createRideFile(lastmark, mark);
|
|
wizard->activities.append(add);
|
|
}
|
|
}
|
|
lastmark = mark;
|
|
}
|
|
|
|
// now lets adjust the starttime to avoid conflicts
|
|
// and record adjustment in a metadata field for transparency
|
|
// it will always conflict with current ride, so we pick that
|
|
// up as a special case.
|
|
// we check against existing rides AND the rides we WILL create
|
|
QString originalFileName = wizard->main->home.absolutePath() + "/" + wizard->rideItem->fileName;
|
|
QList<QDateTime> toBeCreated;
|
|
foreach(RideFile *ride, wizard->activities) {
|
|
|
|
int adjust = 0;
|
|
QStringList conflicts = wizard->conflicts(ride->startTime());
|
|
|
|
while (conflicts.count() || toBeCreated.contains(ride->startTime())) {
|
|
|
|
// if the only conflict is with the original filename
|
|
// and it will be wiped then don't worry about it
|
|
// we also check conflicts of rides the WILl be created
|
|
if (conflicts.count() == 1 &&
|
|
conflicts.at(0) == originalFileName &&
|
|
wizard->keepOriginal == false &&
|
|
!toBeCreated.contains(ride->startTime())) {
|
|
break;
|
|
}
|
|
|
|
adjust++;
|
|
ride->setStartTime(ride->startTime().addSecs(1));
|
|
conflicts = wizard->conflicts(ride->startTime());
|
|
}
|
|
|
|
// record the fact we adjusted...
|
|
if (adjust) {
|
|
ride->setTag("Start Time Adjust", QString("%1 seconds").arg(adjust));
|
|
}
|
|
toBeCreated.append(ride->startTime());
|
|
}
|
|
|
|
wizard->setFilesList();
|
|
}
|
|
|
|
//create a new ride file from the current ride file
|
|
//by using datapoints from index start to stop
|
|
RideFile *
|
|
SplitConfirm::createRideFile(long start, long stop)
|
|
{
|
|
RideFile *returning = new RideFile; // target
|
|
RideFile *ride = wizard->rideItem->ride(); // source
|
|
|
|
// set offset in seconds, make sure in bounds too
|
|
double offset = 0;
|
|
double distanceoffset = 0;
|
|
if (start < ride->dataPoints().count() && start >= 0) {
|
|
offset = ride->dataPoints().at(start)->secs;
|
|
distanceoffset = ride->dataPoints().at(start)->km;
|
|
}
|
|
|
|
// copy first class variables (adjust starttime to include offset)
|
|
returning->setStartTime(ride->startTime().addSecs(offset));
|
|
returning->setRecIntSecs(ride->recIntSecs());
|
|
returning->setDeviceType(ride->deviceType());
|
|
|
|
// lets keep the metadata too
|
|
const_cast<QMap<QString,QString>&>(returning->tags()) = QMap<QString,QString>(ride->tags());
|
|
|
|
// now the dataPoints, check in bounds too!
|
|
for(long i=start; i<stop && i<ride->dataPoints().count(); i++) {
|
|
RideFilePoint *p = ride->dataPoints().at(i);
|
|
|
|
returning->appendPoint(p->secs - offset, // start from zero!
|
|
p->cad, p->hr, p->km - distanceoffset, p->kph,
|
|
p->nm, p->watts, p->alt, p->lon, p->lat,
|
|
p->headwind, p->interval);
|
|
}
|
|
|
|
// lets keep intervals that start in our section truncating them
|
|
// if neccessary (some folks want to keep lap markers)
|
|
double startTime = wizard->rideItem->ride()->dataPoints().at(start)->secs;
|
|
double stopTime = wizard->rideItem->ride()->dataPoints().at(stop)->secs;
|
|
foreach (RideFileInterval interval, wizard->rideItem->ride()->intervals()) {
|
|
|
|
if (interval.start >= startTime && interval.start <= stopTime) {
|
|
if (interval.stop > stopTime)
|
|
returning->addInterval(interval.start - offset, stopTime, interval.name);
|
|
else
|
|
returning->addInterval(interval.start - offset, interval.stop - offset, interval.name);
|
|
}
|
|
}
|
|
return returning;
|
|
}
|
|
|
|
bool
|
|
SplitConfirm::validatePage()
|
|
{
|
|
if(QMessageBox::question(this, "Confirm",
|
|
QString("%1 file(s) will be created.\n\nAre you sure you wish to proceed?")
|
|
.arg(wizard->activities.count()),
|
|
QMessageBox::Ok|QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Ok) {
|
|
|
|
// LETS DO IT NOW!
|
|
// first do we need to remove the current ride?
|
|
if (wizard->keepOriginal == false) {
|
|
|
|
wizard->main->removeCurrentRide();
|
|
QTreeWidgetItem *current = wizard->files->invisibleRootItem()->child(0);
|
|
current->setText(5, "Removed");
|
|
}
|
|
|
|
// whizz through and create each new activity
|
|
int off = wizard->keepOriginal ? 0 : 1; // skip first line to remove or not?
|
|
for(int i=0; i<wizard->activities.count(); i++) {
|
|
|
|
QTreeWidgetItem *current = wizard->files->invisibleRootItem()->child(i+off);
|
|
QString target = wizard->main->home.absolutePath() + "/" + current->text(0);
|
|
|
|
JsonFileReader reader;
|
|
QFile out(target);
|
|
reader.writeRideFile(wizard->activities.at(i), out);
|
|
|
|
current->setText(5, "Saved");
|
|
|
|
wizard->main->addRide(QFileInfo(out).fileName(), true);
|
|
}
|
|
|
|
// now make this page the last (so we can see what was done)
|
|
setTitle("Completed");
|
|
setSubTitle("Split Activity Completed");
|
|
|
|
wizard->done = true;
|
|
|
|
return true;
|
|
|
|
} else return false;
|
|
}
|
|
|
|
bool
|
|
SplitConfirm::isComplete() const
|
|
{
|
|
return !wizard->done;
|
|
}
|