Files
GoldenCheetah/src/MergeActivityWizard.cpp
Mark Liversedge b375e34d0d Tweak Merge Adjust Code
.. logic error mean't the merge adjust only applied
   in one direction and also when ride got longer
   or shorter it was not reflected on the plot.
2014-11-15 09:34:15 +00:00

1188 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 "MainWindow.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"));
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 paramters 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.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.start + offset1,
interval.stop + offset1,
interval.name);
}
// run through what we got then
foreach(RideFileInterval interval, ride2->intervals()) {
combined->addInterval(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
const QTreeWidgetItem *allRides = wizard->context->athlete->allRideItems();
for (int i=allRides->childCount()-1; i>=0; i--) {
RideItem *rideItem = static_cast<RideItem*>(allRides->child(i));
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);
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() / 3);
adjustSlider->setMaximum(wizard->combined->dataPoints().count() / 3);
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);
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 ride with "
" the combined data.\n\n"
"The changes will not be saved until you save them "
" so you can check and revert or save.\n\n"
"If you continue the ride will be updated, 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 -- update BUT DOESNT SAVE
// user can now check !
wizard->current->setRide(wizard->combined);
wizard->context->notifyRideDirty();
return true;
}