Files
GoldenCheetah/src/MergeActivityWizard.cpp
Claus Assmann 2deb532839 Fix compilation error
.. RideItem::rideDataChanged() is protected
2015-09-16 06:53:33 +01:00

1194 lines
38 KiB
C++

/*
* Copyright (c) 2011-13 Damien Grauser (Damien.Grauser@pev-geneve.ch)
* 2014 Mark Liversedgge (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 "MergeActivityWizard.h"
#include "Context.h"
#include "RideCache.h"
#include "MainWindow.h"
#include "HelpWhatsThis.h"
// minimum R-squared fit when trying to find offsets to
// merge ride files. Lower numbers mean happier to take
// and answer that is less likely to be correct, but then
// its possibly better than nothing !
//
// I have found that with real world data, where no data
// has been resampled then fits >0.8 are usual
// We always use the best fit anyway, this is just to
// decide what to discard as not valuable
static const double MINIMUM_R2_FIT = 0.75f;
/*----------------------------------------------------------------------
*
* Page Flow Summary
*
* 10 MergeWelcome Welcome message
*
* 20 MergeSource Select source (import or download)
* 25 MergeDownload Download from device
* 27 MergeChoose Select a ride from a list
*
* 30 MergeMode Select mode (join / merge)
*
* if merge (not join)
* 40 MergeSelect Select series to combine
* 50 MergeStrategy Select strategy for aligning data
* 60 MergeAdjust Manually adjust alignment
*
* 70 MergeConfirm Confirm and Save away
*
*---------------------------------------------------------------------*/
MergeActivityWizard::MergeActivityWizard(Context *context) : QWizard(context->mainWindow), context(context)
{
#ifdef Q_OS_MAC
setWizardStyle(QWizard::ModernStyle);
#endif
setWindowTitle(tr("Combine Activities"));
HelpWhatsThis *help = new HelpWhatsThis(this);
this->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::MenuBar_Activity_CombineRides));
setFixedHeight(530);
setFixedWidth(550);
// initialise before setRide since it checks
// to see if memory needs to be freed first
ride1 = ride2 = NULL;
combinedItem = new RideItem(NULL, context);
combined = NULL;
// current ride
current = const_cast<RideItem*>(context->currentRideItem());
recIntSecs= current->ride()->recIntSecs();
setRide(&ride1, current->ride());
// default to merge not join
// and merge on device clocks
mode = 0;
strategy = 0;
// 5 step process, although Conflict may be skipped
setPage(10, new MergeWelcome(this));
setPage(20, new MergeSource(this));
setPage(25, new MergeDownload(this));
setPage(27, new MergeChoose(this));
setPage(30, new MergeMode(this));
setPage(40, new MergeSelect(this));
setPage(50, new MergeStrategy(this));
setPage(60, new MergeAdjust(this)); // might need to rename to adjust
setPage(1000, new MergeConfirm(this));
}
void
MergeActivityWizard::setRide(RideFile **here, RideFile *with)
{
// wipe current
if (*here) delete *here;
// set with new resampled / filled
// you may be tempted to optimise this out
// but by cloning a working copy like this
// we start with a clean ride and no derived
// data to 'pollute' the process
if (with) *here = with->resample(recIntSecs);
else *here = NULL;
}
void
MergeActivityWizard::analyse()
{
// looking at the parameters determine the offset
// default to align left if all else fails !
offset1 = offset2 = 0;
switch(strategy) {
case 0: // align on the time of day
// assumes the recording devices have
// a clock that is well synchronised
{
double diff = ride1->startTime().toMSecsSinceEpoch() - ride2->startTime().toMSecsSinceEpoch();
int samples = (diff/recIntSecs)/1000.0f;
if (samples < 0) { // ride2 was later than ride 1
offset2 = abs(samples);
offset1 = 0;
} else { // ride1 was later than ride 2
offset1 = samples;
offset2 = 0;
}
// but wait, is that too long ?
if (offset1 > ride1->dataPoints().count() ||
offset2 > ride2->dataPoints().count()) {
// fallback to align same time
offset1 = offset2 = 0;
}
//qDebug()<<"clocks make ride1 offset="<<offset1<<"and ride2 offset="<<offset2;
}
break;
case 1: // align on shared series
// using a shotgun algorithm
{
// calculate the R2 fit using the current offset
// for the first shared series
RideFile *base = ride1;
RideFile *fit = ride2;
// always fit smaller to larger
int diff = ride1->dataPoints().count() - ride2->dataPoints().count();
if (diff < 0) { // ride2 has more points
base=ride2;
fit= ride1;
} else { // ride1 has more points
base=ride1;
fit=ride2;
}
double bestFit=0.0;
int offsetFit=0;
RideFile::SeriesType bestSeries=RideFile::none;
QMapIterator<RideFile::SeriesType, QCheckBox *> i(rightSeries);
while(i.hasNext()) {
i.next();
if (i.key() != RideFile::km && leftSeries.value(i.key(), NULL) != NULL) {
// for each shared series look for best fit
RideFile::SeriesType shared = i.key();
double bestR2 = 0.0f;
int bestOffset = 0;
// no more than shifting by a third of the ride backwards or forwards
for(int offset=-1 * (base->dataPoints().count()/3);
offset<base->dataPoints().count()/3;
offset++) {
double SStot=0.0f, SSres=0.0f;
double mean =0.0f;
int count=0;
for(int i=0; (i+offset)<base->dataPoints().count(); i++) {
if ((i+offset)>0) {
mean += base->dataPoints()[i+offset]->value(shared);
}
count++;
}
mean /= double(count);
for(int i=0; (i+offset)<base->dataPoints().count() && i<fit->dataPoints().count(); i++) {
if((i+offset)>0) {
SSres += pow(base->dataPoints()[i+offset]->value(shared) - fit->dataPoints()[i]->value(shared), 2);
SStot += pow(base->dataPoints()[i+offset]->value(shared) - mean, 2);
}
}
double R2= 1.0f - (SSres/SStot);
if (R2 > bestR2) {
bestR2= R2;
bestOffset=offset;
}
}
// is this a better fit ?
if (bestR2 > bestFit) {
bestFit=bestR2;
offsetFit=bestOffset;
bestSeries=shared;
Q_UNUSED(bestSeries); // shutup compiler when qDebugs commented out below
}
//qDebug()<<"best R2="<<bestR2<<"at best offset"<<bestOffset<<"with series"<<fit->seriesName(shared);
}
}
//qDebug()<<"THEREFORE: best R2="<<bestFit<<"at best offset"<<offsetFit<<"with series"<<ride1->seriesName(bestSeries);
// so lets turn that into an offset for ride1 and ride2
if (bestFit > MINIMUM_R2_FIT) {
if (diff <0) { // ride2 was the base
if (offsetFit<0) {
offset2=offsetFit * -1;
offset1=0;
} else {
offset1=offsetFit;
offset2=0;
}
} else {
if (offsetFit<0) {
offset1=offsetFit * -1;
offset2=0;
} else {
offset2=offsetFit;
offset1=0;
}
}
}
}
break;
case 2: // align begin
offset1=0;
offset2=0;
break;
case 3: // align end
int offset = ride1->dataPoints().count() - ride2->dataPoints().count();
if (offset < 0) {
offset1 = abs(offset);
offset2 = 0;
} else {
offset2 = abs(offset);
offset1 = 0;
}
break;
}
}
void
MergeActivityWizard::combine()
{
// and build a new one
combined = new RideFile(ride1);
combinedItem->setRide(combined); // will delete old one
// create a combined ride applying the parameters
// from the wizard for join or merge
if (mode == 1) { // JOIN
// easy peasy -- loop through one then the other !
RideFilePoint *lp = NULL;
foreach(RideFilePoint *p, ride1->dataPoints()) {
combined->appendPoint(*p);
lp = p;
}
// now add the data from the second one!
double distanceOffset=0;
double timeOffset=0;
bool first=true;
foreach(RideFilePoint *p, ride2->dataPoints()) {
if (first) {
if (lp) {
distanceOffset = lp->km + p->km;
timeOffset = lp->secs + p->secs + recIntSecs;
}
first = false;
}
RideFilePoint add = *p;
add.secs += timeOffset;
add.km += distanceOffset;
combined->appendPoint(add);
}
// any intervals with a number name? find the last
int intervalN=0;
foreach(RideFileInterval *interval, ride1->intervals()) {
int x = interval->name.toInt();
if (interval->name == QString("%1").arg(x)) {
if (x > intervalN) intervalN = x;
}
}
// now run through the intervals for the second ride
// and add them but renumber any intervals that are just numbers
foreach(RideFileInterval *interval, ride2->intervals()) {
int x = interval->name.toInt();
if (interval->name == QString("%1").arg(x)) {
interval->name = QString("%1").arg(x+intervalN);
}
combined->addInterval(interval->type,
interval->start + timeOffset,
interval->stop + timeOffset,
interval->name);
}
} else { // MERGE
RideFilePoint last;
for (int i=0; i<ride1->dataPoints().count() + offset1 ||
i<ride2->dataPoints().count() + offset2; i++) {
// fresh point
RideFilePoint add;
add.secs = i * recIntSecs;
add.km = last.km; // if not getting copied at least stay in same place!
// fold in ride 1 values
if (offset1 <= i && i < ride1->dataPoints().count()+offset1) {
RideFilePoint source = *(ride1->dataPoints()[i-offset1]);
// copy across the data we want
QMapIterator<RideFile::SeriesType,QCheckBox*> i(leftSeries);
while(i.hasNext()) {
i.next();
// we want this series !
if (i.value()->isChecked()) {
add.setValue(i.key(), source.value(i.key()));
}
}
}
// fold in ride 2 values
if (offset2 <= i && i < ride2->dataPoints().count()+offset2) {
RideFilePoint source = *(ride2->dataPoints()[i-offset2]);
// copy across the data we want
QMapIterator<RideFile::SeriesType,QCheckBox*> i(rightSeries);
while(i.hasNext()) {
i.next();
// we want this series !
if (i.value()->isChecked()) {
add.setValue(i.key(), source.value(i.key()));
}
}
}
combined->appendPoint(add);
last = add;
}
// now realign the intervals, first we need to
// clear what we already have in combined
combined->clearIntervals();
// run through what we got then
foreach(RideFileInterval *interval, ride1->intervals()) {
combined->addInterval(interval->type,
interval->start + offset1,
interval->stop + offset1,
interval->name);
}
// run through what we got then
foreach(RideFileInterval *interval, ride2->intervals()) {
combined->addInterval(interval->type,
interval->start + offset2,
interval->stop + offset2,
interval->name);
}
}
}
/*----------------------------------------------------------------------
* Wizard Pages
*--------------------------------------------------------------------*/
// welcome
MergeWelcome::MergeWelcome(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Combine Activities"));
setSubTitle(tr("Lets get started"));
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
QLabel *label = new QLabel(tr("This wizard will help you combine data with "
"the currently selected activity.\n\n"
"You will be able to import or download data before "
"merging or joining the data and manually adjusting "
"the alignment of data series before it is saved."));
label->setWordWrap(true);
layout->addWidget(label);
layout->addStretch();
}
//Select source for merge
MergeSource::MergeSource(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Select Source"));
setSubTitle(tr("Where is the data you want to combine ?"));
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
mapper = new QSignalMapper(this);
connect(mapper, SIGNAL(mapped(QString)), this, SLOT(clicked(QString)));
// select a file
QCommandLinkButton *p = new QCommandLinkButton(tr("Import from a File"),
tr("Import and combine from a file on your hard disk"
" or device mounted as a USB disk to combine with "
"the current activity."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "import");
layout->addWidget(p);
// download from a device
p = new QCommandLinkButton(tr("Download from Device"),
tr("Download data from a serial port device such as a Moxy Muscle Oxygen Monitor or"
" bike computer to combine with the current activity."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "download");
layout->addWidget(p);
// select from rides on same day
p = new QCommandLinkButton(tr("Existing Activity"),
tr("Combine data from an activity that has already been imported or downloaded into"
" GoldenCheetah. Selecting from a list of all available activities. "), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "choose");
layout->addWidget(p);
label = new QLabel("", this);
layout->addWidget(label);
next = 30;
setFinalPage(false);
}
void
MergeSource::initializePage()
{
}
void
MergeSource::clicked(QString p)
{
// reset -- particularly since we might get here from
// other pages hitting 'Back'
initializePage();
// move on if we imported one
if (p == "import" && importFile() == true) {
next = 30;
wizard->next();
}
// move on if we downloaded one
if (p == "download") {
next = 25;
wizard->next();
}
// select from a list
if (p == "choose") {
next = 27;
wizard->next();
}
}
bool
MergeSource::importFile()
{
QVariant lastDirVar = appsettings->value(this, GC_SETTINGS_LAST_IMPORT_PATH);
QString lastDir = (lastDirVar != QVariant())
? lastDirVar.toString() : QDir::homePath();
const RideFileFactory &rff = RideFileFactory::instance();
QStringList suffixList = rff.suffixes();
suffixList.replaceInStrings(QRegExp("^"), "*.");
QStringList fileNames;
QStringList allFormats;
allFormats << QString(tr("All Supported Formats (%1)")).arg(suffixList.join(" "));
foreach(QString suffix, rff.suffixes())
allFormats << QString("%1 (*.%2)").arg(rff.description(suffix)).arg(suffix);
allFormats << tr("All files (*.*)");
fileNames = QFileDialog::getOpenFileNames(
this, tr("Import from File"), lastDir,
allFormats.join(";;"));
if (!fileNames.isEmpty()) {
lastDir = QFileInfo(fileNames.front()).absolutePath();
appsettings->setValue(GC_SETTINGS_LAST_IMPORT_PATH, lastDir);
QStringList fileNamesCopy = fileNames; // QT doc says iterate over a copy
return importFile(fileNamesCopy);
}
return false;
}
bool
MergeSource::importFile(QList<QString> files)
{
// get fullpath name for processing
QFileInfo filename = QFileInfo(files[0]).absoluteFilePath();
QFile thisfile(files[0]);
QFileInfo thisfileinfo(files[0]);
QStringList errors;
if (thisfileinfo.exists() && thisfileinfo.isFile() && thisfileinfo.isReadable()) {
// is it one we understand ?
QStringList suffixList = RideFileFactory::instance().suffixes();
QRegExp suffixes(QString("^(%1)$").arg(suffixList.join("|")));
suffixes.setCaseSensitivity(Qt::CaseInsensitive);
if (suffixes.exactMatch(thisfileinfo.suffix())) {
RideFile *ride = RideFileFactory::instance().openRideFile(wizard->context, thisfile, errors);
// did it parse ok?
if (ride) {
wizard->setRide(&wizard->ride2, ride);
return true;
}
} else {
wizard->setRide(&wizard->ride2, NULL);
errors.append(tr("Error - Unknown file type"));
return false;
}
}
return false;
}
// Download dialog embedded
MergeDownload::MergeDownload(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Download Activity"));
setSubTitle(tr("Download Activity to Combine"));
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
// embed download dialog
QMdiArea *mdiarea = new QMdiArea(this);
layout->addWidget(mdiarea);
// get a download dialog -- embedded
DownloadRideDialog *download = new DownloadRideDialog(parent->context, true);
//download->setInputMode(QInputDialog.TextInput);
QMdiSubWindow *w = mdiarea->addSubWindow(download, Qt::FramelessWindowHint);
// maximize and no title bar etc
w->showMaximized();
layout->addStretch();
connect(download, SIGNAL(downloadStarts()), this, SLOT(downloadStarts()));
connect(download, SIGNAL(downloadEnds()), this, SLOT(downloadEnds()));
connect(download, SIGNAL(downloadFiles(QList<DeviceDownloadFile>)), this,
SLOT(downloadFiles(QList<DeviceDownloadFile>)));
}
void
MergeDownload::downloadFiles(QList<DeviceDownloadFile>files)
{
// Bingo ! only one ride so must be this one
// saves having to select from a pesky list
// XXX WE NEED TO UPDATE DOWNLOADRIDEDIALOG TO
// ALLOW SELECTION OF RIDES TO PROCESS AND
// ALSO MAKE IT LIMIT THIS TO ONE RIDE WHEN
// EMBEDDED IN MERGEDOWNLOAD ! XXX
if (files.count() == 1) {
QFile thisfile(files[0].name);
QStringList errors;
RideFileReader *reader = RideFileFactory::instance().readerForSuffix(files[0].extension);
if (!reader) return;
RideFile *ride = reader->openRideFile(thisfile, errors);
// did it parse ok?
if (ride) {
wizard->setRide(&wizard->ride2 ,ride);
next = 30;
wizard->next();
return;
} else {
wizard->setRide(&wizard->ride2, NULL);
errors.append(tr("Error - Unknown file type"));
return;
}
}
}
void
MergeDownload::downloadStarts()
{
wizard->button(QWizard::BackButton)->setEnabled(false);
}
void
MergeDownload::downloadEnds()
{
wizard->button(QWizard::BackButton)->setEnabled(true);
}
void
MergeDownload::initializePage()
{
// might be needed for download ????
}
// Choose from the ride list
MergeChoose::MergeChoose(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Choose an Activity"));
setSubTitle(tr("Choose an Existing activity to Combine"));
chosen = false;
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
files = new QTreeWidget;
files->headerItem()->setText(0, tr("Filename"));
files->headerItem()->setText(1, tr("Date"));
files->headerItem()->setText(2, tr("Time"));
files->setColumnCount(3);
files->setColumnWidth(0, 190); // filename
files->setColumnWidth(1, 95); // date
files->setColumnWidth(2, 90); // time
files->setSelectionMode(QAbstractItemView::SingleSelection);
files->setUniformRowHeights(true);
files->setIndentation(0);
// populate with each ride in the ridelist
foreach (RideItem *rideItem, wizard->context->athlete->rideCache->rides()) {
QTreeWidgetItem *add = new QTreeWidgetItem(files->invisibleRootItem());
add->setFlags(add->flags() & ~Qt::ItemIsEditable);
// we will wipe the original file
add->setText(0, rideItem->fileName);
add->setText(1, rideItem->dateTime.toString(tr("dd MMM yyyy")));
add->setText(2, rideItem->dateTime.toString("hh:mm:ss"));
}
layout->addWidget(files);
connect(files, SIGNAL(itemSelectionChanged()), this, SLOT(selected()));
}
void
MergeChoose::selected()
{
wizard->button(QWizard::BackButton)->setEnabled(true);
chosen = true;
next = 30;
emit completeChanged();
}
bool
MergeChoose::validatePage()
{
// make sure the currently selected ride has data
// and can be opened etc
QString filename = "none";
// which one is selected ?
if (files->currentItem()) filename=files->currentItem()->text(0);
// open it..
QStringList errors;
QList<RideFile*> rides;
QFile thisfile(QString(wizard->context->athlete->home->activities().absolutePath()+"/"+filename));
RideFile *ride = RideFileFactory::instance().openRideFile(wizard->context, thisfile, errors, &rides);
if (ride && ride->dataPoints().count()) {
wizard->setRide(&wizard->ride2, ride);
return true;
}
return false;
}
void
MergeChoose::initializePage()
{
}
//Select mode for merge
MergeMode::MergeMode(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Select Mode"));
setSubTitle(tr("How would you like to combine the data ?"));
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
mapper = new QSignalMapper(this);
connect(mapper, SIGNAL(mapped(QString)), this, SLOT(clicked(QString)));
// merge
QCommandLinkButton *p = new QCommandLinkButton(tr("Merge Data to add another data series"),
tr("Merge data series from one recording into the current activity"
" where different types of data (e.g. O2 data from a Moxy) have been "
" recorded by different devices. Taking care to align the data in time."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "merge");
layout->addWidget(p);
// join
p = new QCommandLinkButton(tr("Join Data to form a longer activity"),
tr("Append the data to the end of the current activity "
" to create a longer activity that was recorded in multiple"
" parts."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "join");
layout->addWidget(p);
label = new QLabel("", this);
layout->addWidget(label);
next = 40;
setFinalPage(false);
}
void
MergeMode::initializePage()
{
}
void
MergeMode::clicked(QString p)
{
// reset -- particularly since we might get here from
// other pages hitting 'Back'
initializePage();
if (p == "join") {
wizard->mode = 1; // join is easy !
wizard->combine(); // analyse not required just combined them !
next = 1000;
} else {
// where to next ?
wizard->mode = 0; // merge ...
next = 40;
}
wizard->next();
}
//Select merge strategy
MergeStrategy::MergeStrategy(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Select Strategy"));
setSubTitle(tr("How should we align the data ?"));
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
mapper = new QSignalMapper(this);
connect(mapper, SIGNAL(mapped(QString)), this, SLOT(clicked(QString)));
// time
QCommandLinkButton *p = new QCommandLinkButton(tr("Align using start time"),
tr("Align the data from the two activities based upon the start time "
"for the activities. This will work well if the devices used to "
"record the data have their clocks synchronised / close to each other."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "time");
layout->addWidget(p);
// shared data
shared = new QCommandLinkButton(tr("Align using shared data series"),
tr("If the two activities both contain the same data series, for example "
"where both devices recorded cadence or perhaps HR, then we can align the other "
"data series in time by matching the peaks and troughs in the shared data."), this);
connect(shared, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(shared, "shared");
layout->addWidget(shared);
// start at same time
p = new QCommandLinkButton(tr("Align starting together"),
tr("Regardless of the timestamp on the activity, align with both "
"activities starting at the same time."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "left");
layout->addWidget(p);
// start at same time
p = new QCommandLinkButton(tr("Align ending together"),
tr("Regardless of the timestamp on the activity, align with both "
"activities ending at the same time."), this);
connect(p, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(p, "right");
layout->addWidget(p);
label = new QLabel("", this);
layout->addWidget(label);
next = 60;
setFinalPage(false);
}
void
MergeStrategy::initializePage()
{
// are there any shared data -- ignoring time and distance
bool hasShared = false;
QMapIterator<RideFile::SeriesType, QCheckBox *> i(wizard->rightSeries);
while(i.hasNext()) {
i.next();
if (i.key() != RideFile::km && wizard->leftSeries.value(i.key(), NULL) != NULL)
hasShared = true;
}
shared->setEnabled(hasShared);
}
void
MergeStrategy::clicked(QString p)
{
// reset -- particularly since we might get here from
// other pages hitting 'Back'
initializePage();
if (p == "time") {
wizard->strategy = 0;
} else if (p == "shared" ) {
// where to next ?
wizard->strategy = 1; // merge ...
} else if (p == "left" ) {
wizard->strategy = 2; // merge ...
} else if (p == "right" ) {
wizard->strategy = 3; // merge ...
}
// now run strategy and get on
wizard->analyse();
wizard->combine();
// lets do this thing !
next = 60;
wizard->next();
}
// Synchronise start of files
MergeAdjust::MergeAdjust(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Adjust Alignment"));
setSubTitle(tr("Adjust merge alignment in time"));
// need more space on this page!
setContentsMargins(0,0,0,0);
// Plot files
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
layout->setSpacing(5);
layout->setContentsMargins(5,5,5,5);
spanSlider = new QxtSpanSlider(Qt::Horizontal, this);
spanSlider->setFocusPolicy(Qt::NoFocus);
spanSlider->setHandleMovementMode(QxtSpanSlider::NoOverlapping);
spanSlider->setLowerValue(0);
spanSlider->setUpperValue(15);
#ifdef Q_OS_MAC
// BUG in QMacStyle and painting of spanSlider
// so we use a plain style to avoid it, but only
// on a MAC, since win and linux are fine
#if QT_VERSION > 0x5000
QStyle *style = QStyleFactory::create("fusion");
#else
QStyle *style = QStyleFactory::create("Cleanlooks");
#endif
spanSlider->setStyle(style);
#endif
fullPlot = new AllPlot(this, NULL, wizard->context);
fullPlot->setPaintBrush(0);
#ifdef Q_OS_MAC
fullPlot->setFixedHeight(300);
#else
fullPlot->setFixedHeight(300);
#endif
fullPlot->setHighlightIntervals(false);
static_cast<QwtPlotCanvas*>(fullPlot->canvas())->setBorderRadius(0);
fullPlot->setWantAxis(false, true);
QPalette pal = palette();
fullPlot->axisWidget(QwtPlot::xBottom)->setPalette(pal);
layout->addWidget(spanSlider);
layout->addWidget(fullPlot);
layout->addStretch();
QLabel *adjust = new QLabel(tr("Adjust:"));
adjustSlider = new QSlider(Qt::Horizontal, this);
reset = new QPushButton(tr("Reset"));
QHBoxLayout *hl = new QHBoxLayout;
hl->addWidget(adjust);
hl->addWidget(adjustSlider);
hl->addWidget(reset);
layout->addLayout(hl);
layout->addStretch();
connect(spanSlider, SIGNAL(lowerPositionChanged(int)), this, SLOT(zoomChanged()));
connect(spanSlider, SIGNAL(upperPositionChanged(int)), this, SLOT(zoomChanged()));
connect(reset, SIGNAL(clicked()), this, SLOT(resetClicked()));
connect(adjustSlider, SIGNAL(valueChanged(int)), this, SLOT(offsetChanged()));
}
void
MergeAdjust::initializePage()
{
// remember so we can reset
offset1 = wizard->offset1;
offset2 = wizard->offset2;
// setup plot
fullPlot->setDataFromRide(wizard->combinedItem, QList<UserData*>());
spanSlider->setMinimum(0);
spanSlider->setMaximum(wizard->combined->dataPoints().last()->secs);
spanSlider->setLowerValue(spanSlider->minimum());
spanSlider->setUpperValue(spanSlider->maximum());
zoomChanged();
// what to show?
fullPlot->setShow(RideFile::none, false); // switch all off
// now add in what we got
QMapIterator<RideFile::SeriesType, QCheckBox *> i(wizard->rightSeries);
while(i.hasNext()) {
i.next();
if (i.value()->isChecked())
fullPlot->setShow(i.key(), true);
}
QMapIterator<RideFile::SeriesType, QCheckBox *> j(wizard->leftSeries);
while(j.hasNext()) {
j.next();
if (j.value()->isChecked())
fullPlot->setShow(j.key(), true);
}
// setup adjuster
adjustSlider->setMinimum(-1 * wizard->combined->dataPoints().count());
adjustSlider->setMaximum(wizard->combined->dataPoints().count());
adjustSlider->setValue(wizard->offset2 - wizard->offset1);
}
void
MergeAdjust::offsetChanged()
{
if (adjustSlider->value() < 0) {
wizard->offset1 = adjustSlider->value() * -1;
wizard->offset2 = 0;
} else {
wizard->offset1 = 0;
wizard->offset2 = adjustSlider->value();
}
wizard->combine();
fullPlot->setDataFromRide(wizard->combinedItem, QList<UserData*>());
bool rescale = (spanSlider->minimum() == spanSlider->lowerValue() &&
spanSlider->maximum() == spanSlider->upperValue());
spanSlider->setMinimum(0);
spanSlider->setMaximum(wizard->combined->dataPoints().last()->secs);
if (rescale) {
spanSlider->setLowerValue(spanSlider->minimum());
spanSlider->setUpperValue(spanSlider->maximum());
zoomChanged();
}
}
void
MergeAdjust::resetClicked()
{
wizard->offset1 = offset1;
wizard->offset2 = offset2;
initializePage();
}
void
MergeAdjust::zoomChanged()
{
fullPlot->setAxisScale(QwtPlot::xBottom, spanSlider->lowerValue()/60.0f, spanSlider->upperValue()/60.0f);
fullPlot->replot();
}
// select
MergeSelect::MergeSelect(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Merge Data Series"));
setSubTitle(tr("Select the series to merge together"));
QVBoxLayout *mainlayout = new QVBoxLayout(this);
QFont def;
def.setWeight(QFont::Bold);
QHBoxLayout *names = new QHBoxLayout;
leftName = new QLabel("Current Selection", this);
rightName = new QLabel("right", this);
leftName->setFixedHeight(20);
leftName->setFont(def);
leftName->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
rightName->setFixedHeight(20);
rightName->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
rightName->setFont(def);
names->addWidget(leftName);
names->addWidget(rightName);
mainlayout->addLayout(names);
QHBoxLayout *leftright = new QHBoxLayout;
mainlayout->addLayout(leftright);
QScrollArea *left = new QScrollArea(this);
left->setAutoFillBackground(false);
left->setWidgetResizable(true);
left->setContentsMargins(0,0,0,0);
QScrollArea *right = new QScrollArea(this);
right->setAutoFillBackground(false);
right->setWidgetResizable(true);
right->setContentsMargins(0,0,0,0);
leftright->addWidget(left);
leftright->addWidget(right);
// checkboxes on the left
QWidget *leftWidget = new QWidget(this);
leftWidget->setContentsMargins(0,0,0,0);
leftLayout = new QVBoxLayout(leftWidget);
leftLayout->setContentsMargins(0,0,0,0);
leftLayout->setSpacing(0);
// checkboxes on the right
QWidget *rightWidget = new QWidget(this);
rightWidget->setContentsMargins(0,0,0,0);
rightLayout = new QVBoxLayout(rightWidget);
rightLayout->setContentsMargins(0,0,0,0);
rightLayout->setSpacing(0);
left->setWidget(leftWidget);
right->setWidget(rightWidget);
}
void
MergeSelect::initializePage()
{
rightName->setText(QString("%1 (%2)").arg(wizard->ride2->startTime().toString("d MMM yy hh:mm:ss"))
.arg(wizard->ride2->deviceType()));
// create a checkbox for each series present in
QMapIterator<RideFile::SeriesType, QCheckBox*> i(wizard->leftSeries);
while(i.hasNext()) {
i.next();
delete i.value();
}
QMapIterator<RideFile::SeriesType, QCheckBox*> j(wizard->rightSeries);
while(j.hasNext()) {
j.next();
delete j.value();
}
wizard->leftSeries.clear();
wizard->rightSeries.clear();
// get rid of the stretch
leftLayout->takeAt(0);
rightLayout->takeAt(0);
for(int i=0; i < static_cast<int>(RideFile::none); i++) {
// save us casting all the time
RideFile::SeriesType series = static_cast<RideFile::SeriesType>(i);
// the one thing we don't select !
if (series == RideFile::secs) continue;
bool lefthas = false;
if (wizard->ride1->isDataPresent(series)) {
lefthas = true;
QCheckBox *add = new QCheckBox(wizard->ride1->seriesName(series));
add->setChecked(true);
add->setEnabled(false);
leftLayout->addWidget(add);
wizard->leftSeries.insert(series, add);
}
if (wizard->ride2->isDataPresent(series)) {
QCheckBox *add = new QCheckBox(wizard->ride2->seriesName(series));
add->setChecked(!lefthas);
rightLayout->addWidget(add);
wizard->rightSeries.insert(series, add);
connect(add, SIGNAL(stateChanged(int)), this, SLOT(checkboxes()));
}
}
leftLayout->addStretch();
rightLayout->addStretch();
}
void
MergeSelect::checkboxes()
{
// as we turn on/off right side checkboxes we need to
// turn off/on left side checkboxes
QMapIterator<RideFile::SeriesType,QCheckBox*> i(wizard->rightSeries);
while(i.hasNext()) {
i.next();
QCheckBox *left=NULL;
if ((left=wizard->leftSeries.value(i.key(), NULL)) != NULL) {
left->setChecked(!i.value()->isChecked());
}
}
}
MergeConfirm::MergeConfirm(MergeActivityWizard *parent) : QWizardPage(parent), wizard(parent)
{
setTitle(tr("Confirm"));
setSubTitle(tr("Complete and Save"));
QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);
QLabel *label = new QLabel(tr("Press Finish to update the current activity with "
" the combined data.\n\n"
"The changes will be saved and cannot be undone.\n\n"
"If you press continue the activity will be saved, if you "
"do not want to continue either go back and change "
"the settings or press cancel to abort."));
label->setWordWrap(true);
layout->addWidget(label);
layout->addStretch();
}
bool
MergeConfirm::validatePage()
{
// We are done -- recalculate derived series, save and mark done
wizard->current->notifyRideDataChanged();
wizard->combined->recalculateDerivedSeries(true);
wizard->current->setRide(wizard->combined);
wizard->context->mainWindow->saveSilent(wizard->context, wizard->current);
wizard->current->setDirty(false); // lose changes
return true;
}