Files
GoldenCheetah/src/Gui/RideImportWizard.cpp
Joern a147165a45 Ride File Naming with "." allowed
... fix regression allowing multiple "." in Ride File names (since some services are creating such names)
2017-12-17 19:57:32 +01:00

1279 lines
49 KiB
C++

/*
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
*
* 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 "Context.h"
#include "Athlete.h"
#include "MainWindow.h"
#include "RideItem.h"
#include "RideFile.h"
#include "RideImportWizard.h"
#include "RideCache.h"
#include "RideAutoImportConfig.h"
#include "HelpWhatsThis.h"
#include "Settings.h"
#include "Colors.h"
#include "Units.h"
#include "GcRideFile.h"
#include "JsonRideFile.h"
#include "TcxRideFile.h" // for opening multi-ride file
#include "DataProcessor.h"
#include "RideMetadata.h" // for linked defaults processing
#include <QDebug>
#include <QWaitCondition>
#include <QMessageBox>
// drag and drop passes urls ... convert to a list of files and call main constructor
RideImportWizard::RideImportWizard(QList<QUrl> *urls, Context *context, QWidget *parent) : QDialog(parent), context(context)
{
_importInProcess = true;
setAttribute(Qt::WA_DeleteOnClose);
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
QList<QString> filenames;
for (int i=0; i<urls->count(); i++)
filenames.append(QFileInfo(urls->value(i).toLocalFile()).absoluteFilePath());
autoImportMode = false;
autoImportStealth = false;
init(filenames, context);
filenames.clear();
_importInProcess = false;
}
RideImportWizard::RideImportWizard(QList<QString> files, Context *context, QWidget *parent) : QDialog(parent), context(context)
{
_importInProcess = true;
setAttribute(Qt::WA_DeleteOnClose);
autoImportMode = false;
autoImportStealth = false;
init(files, context);
_importInProcess = false;
}
RideImportWizard::RideImportWizard(RideAutoImportConfig *dirs, Context *context, QWidget *parent) : QDialog(parent), context(context), importConfig(dirs)
{
_importInProcess = true;
autoImportMode = true;
autoImportStealth = true;
if (autoImportStealth) hide();
QList<QString> files;
// get the directories & rules
QList<RideAutoImportRule> rules = importConfig->getConfig();
// prepare the widget to show the status of the directory
directoryWidget = new QTableWidget(rules.count(), 3, this);
directoryWidget->verticalHeader()->setDefaultSectionSize(20 *dpiYFactor);
QTableWidgetItem *directoryHeading = new QTableWidgetItem;
directoryHeading->setText(tr("Directory"));
directoryWidget->setHorizontalHeaderItem(0, directoryHeading);
QTableWidgetItem *importRuleHeading = new QTableWidgetItem;
importRuleHeading->setText(tr("Import Rule"));
directoryWidget->setHorizontalHeaderItem(1, importRuleHeading);
QTableWidgetItem *statusHeading = new QTableWidgetItem;
statusHeading->setText(tr("Directory Status"));
directoryWidget->setHorizontalHeaderItem(2, statusHeading);
// and get the allowed files formats
const RideFileFactory &rff = RideFileFactory::instance();
QStringList suffixList = rff.suffixes();
suffixList.replaceInStrings(QRegExp("^"), "*.");
QStringList allFormats;
foreach(QString suffix, rff.suffixes())
allFormats << QString("*.%1").arg(suffix);
// Fill in the directory names and importRuleStatus
int i=-1;
foreach (RideAutoImportRule rule, rules){
i++; // do it here to allow "continue" - and start with "0"
QTableWidgetItem *t;
// Directory
t = new QTableWidgetItem();
t->setText(rule.getDirectory());
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
directoryWidget->setItem(i,0,t);
// Import Rule
QList<QString> descriptions = rule.getRuleDescriptions();
t = new QTableWidgetItem();
t->setText(descriptions.at(rule.getImportRule()));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
directoryWidget->setItem(i,1,t);
// Import Status
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
directoryWidget->setItem(i,2,t);
// only add files if configured to do so
if (rule.getImportRule() == RideAutoImportRule::noImport) {
directoryWidget->item(i,2)->setText(tr("No import"));
continue;
}
// do some checks on the directory first
QString currentImportDirectory = rule.getDirectory();
if (currentImportDirectory == "") {
directoryWidget->item(i,2)->setText(tr("No directory"));
continue;
}
QDir *importDir = new QDir (currentImportDirectory);
if (!importDir->exists()) { // directory might not be available (USB,..)
directoryWidget->item(i,2)->setText(tr("Directory not available"));
continue;
}
if (!importDir->isReadable()) {
directoryWidget->item(i,2)->setText(tr("Directory not readable"));
continue;
}
// determine timerange in the past which should considerd in import
QDate selectAfter = QDate::currentDate();
switch(rule.getImportRule()) {
case RideAutoImportRule::importLast90days:
case RideAutoImportRule::importBackground90:
selectAfter = selectAfter.addDays(Q_INT64_C(-90));
break;
case RideAutoImportRule::importLast180days:
case RideAutoImportRule::importBackground180:
selectAfter = selectAfter.addDays(Q_INT64_C(-180));
break;
case RideAutoImportRule::importLast360days:
case RideAutoImportRule::importBackground360:
selectAfter = selectAfter.addDays(Q_INT64_C(-360));
break;
}
// if any of the rules says "with Dialog" then we keep the dialog - if not it's stealth
switch (rule.getImportRule()) {
case RideAutoImportRule::importAll:
case RideAutoImportRule::importLast90days:
case RideAutoImportRule::importLast180days:
case RideAutoImportRule::importLast360days:
autoImportStealth = false;
break;
}
// now get the files with their full names
QFileInfoList fileInfos = importDir->entryInfoList(allFormats, QDir::Files, QDir::NoSort);
if (!fileInfos.isEmpty()) {
int j = 0;
foreach(QFileInfo f, fileInfos) {
// append following the import rules
switch (rule.getImportRule()) {
case RideAutoImportRule::importAll:
case RideAutoImportRule::importBackgroundAll:
files.append(f.absoluteFilePath());
j++;
break;
case RideAutoImportRule::importLast90days:
case RideAutoImportRule::importLast180days:
case RideAutoImportRule::importLast360days:
case RideAutoImportRule::importBackground90:
case RideAutoImportRule::importBackground180:
case RideAutoImportRule::importBackground360:
if (f.created().date() >= selectAfter || f.lastModified().date() >= selectAfter) {
files.append(f.absoluteFilePath());
j++;
};
break;
}
}
if (j > 0) {
directoryWidget->item(i,2)->setText(tr("%1 files for import selected").arg(QString::number(j)));
} else {
directoryWidget->item(i,2)->setText(tr("No files in requested time range"));
}
} else {
directoryWidget->item(i,2)->setText(tr("No activity files found"));
continue;
}
}
directoryWidget->setColumnWidth(0, 400 *dpiXFactor);
directoryWidget->setColumnWidth(1, 250 *dpiXFactor);
directoryWidget->setColumnWidth(2, 230 *dpiXFactor);
init(files, context);
_importInProcess = false;
}
void
RideImportWizard::init(QList<QString> files, Context * /*mainWindow*/)
{
// setup Help
HelpWhatsThis *help = new HelpWhatsThis(this);
this->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::MenuBar_Activity_Import));
// initialise dialog box
tableWidget = new QTableWidget(files.count(), 6, this);
tableWidget->setItemDelegate(new RideDelegate(1)); // use a delegate for column 1 date
tableWidget->verticalHeader()->setDefaultSectionSize(20 *dpiXFactor);
phaseLabel = new QLabel;
progressBar = new QProgressBar();
todayButton = new QComboBox();
todayButton->addItem(tr("Select Date..."));
todayButton->addItem(tr("Today"));
todayButton->addItem(tr("Last Monday"));
todayButton->addItem(tr("Last Tuesday"));
todayButton->addItem(tr("Last Wednesday"));
todayButton->addItem(tr("Last Thursday"));
todayButton->addItem(tr("Last Friday"));
todayButton->addItem(tr("Last Saturday"));
todayButton->addItem(tr("Last Sunday"));
todayButton->addItem(tr("Choose Date"));
cancelButton = new QPushButton(tr("Cancel"));
abortButton = new QPushButton(tr("Abort"));
//overFiles = new QCheckBox(tr("Overwrite Existing Files")); // deprecate for this release... XXX
// initially the cancel, overwrite and today widgets are hidden
// they only appear whilst we are asking the user for dates
cancelButton->setHidden(true);
todayButton->setHidden(true);
//overFiles->setHidden(true); // deprecate for this release... XXX
//overwriteFiles = false;
aborted = false;
// NOTE: abort button morphs into save and finish button later
connect(abortButton, SIGNAL(clicked()), this, SLOT(abortClicked()));
// only used when editing dates
connect(todayButton, SIGNAL(activated(int)), this, SLOT(todayClicked(int)));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked()));
// connect(overFiles, SIGNAL(clicked()), this, SLOT(overClicked())); // deprecate for this release... XXX
// title & headings
setWindowTitle(tr("Import Files"));
QTableWidgetItem *filenameHeading = new QTableWidgetItem;
filenameHeading->setText(tr("Filename"));
tableWidget->setHorizontalHeaderItem(0, filenameHeading);
QTableWidgetItem *dateHeading = new QTableWidgetItem;
dateHeading->setText(tr("Date"));
tableWidget->setHorizontalHeaderItem(1, dateHeading);
QTableWidgetItem *timeHeading = new QTableWidgetItem;
timeHeading->setText(tr("Time"));
tableWidget->setHorizontalHeaderItem(2, timeHeading);
QTableWidgetItem *durationHeading = new QTableWidgetItem;
durationHeading->setText(tr("Duration"));
tableWidget->setHorizontalHeaderItem(3, durationHeading);
QTableWidgetItem *distanceHeading = new QTableWidgetItem;
distanceHeading->setText(tr("Distance"));
tableWidget->setHorizontalHeaderItem(4, distanceHeading);
QTableWidgetItem *statusHeading = new QTableWidgetItem;
statusHeading->setText(tr("Import Status"));
tableWidget->setHorizontalHeaderItem(5, statusHeading);
// save target dir for the file import
this->homeImports = context->athlete->home->imports();
this->homeActivities = context->athlete->home->activities();
this->tmpActivities = context->athlete->home->tmpActivities();
// Fill in the filenames and all the textItems
for (int i=0; i < files.count(); i++) {
QTableWidgetItem *t;
filenames.append(QFileInfo(files[i]).canonicalFilePath());
blanks.append(true); // by default editable
// Filename
t = new QTableWidgetItem();
if (autoImportMode)
t->setText(QFileInfo(files[i]).canonicalFilePath());
else
t->setText(QFileInfo(files[i]).fileName());
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(i,0,t);
// Date
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() | Qt::ItemIsEditable);
t->setBackgroundColor(Qt::red);
tableWidget->setItem(i,1,t);
// Time
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() | Qt::ItemIsEditable);
tableWidget->setItem(i,2,t);
// Duration
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(i,3,t);
// Distance
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(i,4,t);
// Import Status
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(i,5,t);
}
// put into our dialog box
// layout
QHBoxLayout *buttons = new QHBoxLayout;
buttons->addWidget(phaseLabel);
buttons->addStretch();
buttons->addWidget(todayButton);
buttons->addStretch();
// buttons->addWidget(overFiles); // deprecate for this release... XXX
buttons->addWidget(cancelButton);
buttons->addWidget(abortButton);
QVBoxLayout *contents = new QVBoxLayout(this);
if (autoImportMode) {
contents->addWidget(directoryWidget);
// only show table if files are available in autoimportMode
if (files.count() == 0) {
tableWidget->setVisible(false);
progressBar->setVisible(false);
}
}
contents->addWidget(tableWidget);
contents->addWidget(progressBar);
contents->addLayout(buttons);
setLayout(contents);
// adjust all the sizes to look tidy
tableWidget->setColumnWidth(0, 200*dpiXFactor); // filename
tableWidget->setColumnWidth(1, 120*dpiXFactor); // date
tableWidget->setColumnWidth(2, 120*dpiXFactor); // time
tableWidget->setColumnWidth(3, 100*dpiXFactor); // duration
tableWidget->setColumnWidth(4, 70*dpiXFactor); // distance
tableWidget->setColumnWidth(5, 250*dpiXFactor); // status
tableWidget->horizontalHeader()->setStretchLastSection(true);
// max height for 16 items and a scrollbar on right if > 16 items
// for some reason the window is wider for 10-16 items too.
// someone that understands width/geometry and layouts better
// than me should clean up the logic here, no doubt using a
// single call to geometry/hint. But for now it looks good
// on Leopard, no doubt not so good on Windows
resize((920 +
((files.count() > 16 ? 24 : 0) +
((files.count() > 9 && files.count() < 17) ? 8 : 0)))*dpiXFactor,
(118 + ((files.count() > 16 ? 17*20 : (files.count()+1) * 20)
+ ((autoImportMode) ? 100 : 0)))*dpiYFactor); // assume not more the 5 directory in average
if (autoImportMode) directoryWidget->adjustSize();
tableWidget->adjustSize();
// set number of files / so that a caller of the constructor can decide what to do
numberOfFiles = files.count();
}
int
RideImportWizard::getNumberOfFiles() {
return numberOfFiles;
}
int
RideImportWizard::process()
{
// import process is starting
_importInProcess = true;
// Make visible and put in front prior to running down the list & processing...
if (!autoImportStealth) {
if (!isActiveWindow()) activateWindow();
this->show();
}
// set progress bar limits - for each file we
// will make 5 passes over the files
// 1. checking if a file is readable
// 2. parsing it with the RideFileReader
// 3. [optional] collect date/time information from user
// 4. copy file into Library
// 5. Process for CPI (not implemented yet)
// So, therefore the progress bar runs from 0 to files*4. (since step 5 is not implemented yet)
progressBar->setMinimum(0);
progressBar->setMaximum(filenames.count()*4);
// Pass one - Is it valid?
phaseLabel->setText(tr("Step 1 of 4: Check file permissions"));
for (int i=0; i < filenames.count(); i++) {
// get fullpath name for processing
QFileInfo thisfile(filenames[i]);
if (!thisfile.exists()) tableWidget->item(i,5)->setText(tr("Error - File does not exist."));
else if (!thisfile.isFile()) tableWidget->item(i,5)->setText(tr("Error - Not a file."));
else if (!thisfile.isReadable()) tableWidget->item(i,5)->setText(tr("Error - File is not readable."));
else {
// is it one we understand ?
QStringList suffixList = RideFileFactory::instance().suffixes();
QRegExp suffixes(QString("^(%1)$").arg(suffixList.join("|")));
suffixes.setCaseSensitivity(Qt::CaseInsensitive);
// strip off gz or zip as openRideFile will sort that for us
// since some file names contain "." as separator, not only for suffixes,
// find the file-type suffix in a 2 step approach
QStringList allNameParts = thisfile.fileName().split(".");
QString suffix = tr("undefined");
if (!allNameParts.isEmpty()) {
if (allNameParts.last().toLower() == "zip" ||
allNameParts.last().toLower() == "gz") {
// gz/zip are handled by openRideFile
allNameParts.removeLast();
}
if (!allNameParts.isEmpty()) {
suffix = allNameParts.last();
}
}
if (suffixes.exactMatch(suffix)) {
// Woot. We know how to parse this baby
tableWidget->item(i,5)->setText(tr("Queued"));
} else {
tableWidget->item(i,5)->setText(tr("Error - Unknown file type") + ": " + suffix);
}
}
progressBar->setValue(progressBar->value()+1);
}
if (aborted) { done(0); return 0; }
repaint();
QApplication::processEvents();
// Pass 2 - Read in with the relevant RideFileReader method
phaseLabel->setText(tr("Step 2 of 4: Validating Files"));
for (int i=0; i< filenames.count(); i++) {
// does the status say Queued?
if (!tableWidget->item(i,5)->text().startsWith(tr("Error"))) {
QStringList errors;
QFile thisfile(filenames[i]);
tableWidget->item(i,5)->setText(tr("Parsing..."));
tableWidget->setCurrentCell(i,5);
QApplication::processEvents();
if (aborted) { done(0); return 0; }
this->repaint();
QApplication::processEvents();
QList<RideFile*> rides;
RideFile *ride = RideFileFactory::instance().openRideFile(context, thisfile, errors, &rides);
// is this an archive of files?
if (rides.count() > 1) {
int here = i;
// remove current filename from state arrays and tableview
filenames.removeAt(here);
blanks.removeAt(here);
tableWidget->removeRow(here);
// resize dialog according to the number of rows we expect
int willhave = filenames.count() + rides.count();
resize((920 + ((willhave > 16 ? 24 : 0) +
((willhave > 9 && willhave < 17) ? 8 : 0)))*dpiXFactor,
(118 + ((willhave > 16 ? 17*20 : (willhave+1) * 20)))*dpiYFactor);
// ok so create a temporary file and add to the tableWidget
// we write as JSON to ensure we don't lose data e.g. XDATA.
int counter = 0;
foreach(RideFile *extracted, rides) {
// write as a temporary file, using the original
// filename with "-n" appended
QString fulltarget = QDir::tempPath() + "/" + QFileInfo(thisfile).baseName() + QString("-%1.json").arg(counter+1);
JsonFileReader reader;
QFile target(fulltarget);
reader.writeRideFile(context, extracted, target);
deleteMe.append(fulltarget);
delete extracted;
// now add each temporary file ...
filenames.insert(here, fulltarget);
blanks.insert(here, true); // by default editable
tableWidget->insertRow(here+counter);
QTableWidgetItem *t;
// Filename
t = new QTableWidgetItem();
t->setText(fulltarget);
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(here+counter,0,t);
// Date
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() | Qt::ItemIsEditable);
t->setBackgroundColor(Qt::red);
tableWidget->setItem(here+counter,1,t);
// Time
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() | Qt::ItemIsEditable);
tableWidget->setItem(here+counter,2,t);
// Duration
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(here+counter,3,t);
// Distance
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(here+counter,4,t);
// Import Status
t = new QTableWidgetItem();
t->setText(tr(""));
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
tableWidget->setItem(here+counter,5,t);
counter++;
tableWidget->adjustSize();
}
QApplication::processEvents();
// progress bar needs to adjust...
progressBar->setMaximum(filenames.count()*4);
// then go back one and re-parse from there
rides.clear();
i--;
goto next; // buttugly I know, but count em across 100,000 lines of code
}
// did it parse ok?
if (ride) {
// ride != NULL but !errors.isEmpty() means they're just warnings
if (errors.isEmpty())
tableWidget->item(i,5)->setText(tr("Validated"));
else {
tableWidget->item(i,5)->setText(tr("Warning - ") + errors.join(tr(";")));
}
// Set Date and Time
if (!ride->startTime().isValid()) {
// Poo. The user needs to supply the date/time for this ride
blanks[i] = true;
tableWidget->item(i,1)->setText(tr(""));
tableWidget->item(i,2)->setText(tr(""));
} else {
// Cool, the date and time was extracted from the source file
blanks[i] = false;
tableWidget->item(i,1)->setText(ride->startTime().date().toString(Qt::ISODate));
tableWidget->item(i,2)->setText(ride->startTime().toString("hh:mm:ss"));
}
tableWidget->item(i,1)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); // put in the middle
tableWidget->item(i,2)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); // put in the middle
// time and distance from tags (.gc files)
QMap<QString,QString> lookup;
lookup = ride->metricOverrides.value("total_distance");
double km = lookup.value("value", "0.0").toDouble();
lookup = ride->metricOverrides.value("workout_time");
int secs = lookup.value("value", "0.0").toDouble();
// show duration by looking at last data point
if (!ride->dataPoints().isEmpty() && ride->dataPoints().last() != NULL) {
if (!secs) secs = ride->dataPoints().last()->secs + ride->recIntSecs();
if (!km) km = ride->dataPoints().last()->km;
}
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);
tableWidget->item(i,3)->setText(time);
tableWidget->item(i,3)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); // put in the middle
// show distance by looking at last data point
QString dist = context->athlete->useMetricUnits
? QString ("%1 km").arg(km, 0, 'f', 1)
: QString ("%1 mi").arg(km * MILES_PER_KM, 0, 'f', 1);
tableWidget->item(i,4)->setText(dist);
tableWidget->item(i,4)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
delete ride;
} else {
// nope - can't handle this file
tableWidget->item(i,5)->setText(tr("Error - ") + errors.join(tr(";")));
}
}
progressBar->setValue(progressBar->value()+1);
QApplication::processEvents();
if (aborted) { done(0); return 0; }
this->repaint();
next:;
}
// Pass 3 - get missing date and times for imported files
// Actually allow us to edit date on ANY ride, we
// make sure that the ride date/time is set from
// the filename and never from the ride data
int needdates=0;
for (int i=0; i<filenames.count(); i++) {
// ignore errors
QTableWidgetItem *t = tableWidget->item(i,5);
if (t->text().startsWith(tr("Error"))) continue;
if (blanks[i]) needdates++; // count the blanks tho -- these MUST be edited
// does nothing for the moment
progressBar->setValue(progressBar->value()+1);
progressBar->repaint();
}
// lets make the text more helpful!
if (needdates) {
phaseLabel->setText(QString(tr("Step 3 of 4: %1 ride(s) are missing the date and time.")).arg(needdates));
} else {
phaseLabel->setText(tr("Step 3 of 4: Change Date/Time or Save to continue."));
}
// get it on top to save / correct missing dates
if (autoImportStealth && needdates > 0) {
// leave the stealth mode
this->show();
activateWindow();
} else {
if (!isActiveWindow()) activateWindow();
}
// Wait for user to press save
abortButton->setText(tr("Save"));
aborted = false;
cancelButton->setHidden(false);
todayButton->setHidden(false);
//overFiles->setHidden(false); // deprecate for this release... XXX
if (needdates == 0) {
// no need to wait for the user to input dates
// and press save if all the dates are set
// (i.e. they got set by the file importers already)
// do nothing for now since we need to confirm dates
// and confirm overwrite files rather than importing
// without user intervention
abortButton->setDisabled(false);
activateSave();
if (autoImportStealth) abortClicked(); // simulate "Save" by User
} else {
// de-activate Save button until the dates and times are sorted
abortButton->setDisabled(true);
}
connect(tableWidget, SIGNAL(itemChanged(QTableWidgetItem *)), this, SLOT(activateSave()));
// in autoimport mode / no files, skip "Save",... and goto "Finish"
if (autoImportMode && filenames.count()== 0) {
cancelButton->setHidden(true);
todayButton->setHidden(true);
abortButton->setDisabled(false);
phaseLabel->setText(tr("No files for automatic import selected."));
abortButton->setText(tr("Finish"));
aborted = false;
}
return 0;
}
//void
//RideImportWizard::overClicked()
//{
//overwriteFiles = overFiles->isChecked(); //deprecate in this release XXX
//}
void
RideImportWizard::activateSave()
{
for (int i=0; i<filenames.count(); i++) {
// ignore errors
QTableWidgetItem *t = tableWidget->item(i,5);
if (t->text().startsWith(tr("Error"))) continue;
// date needed?
t = tableWidget->item(i,1);
if (t->text() == "") return;
// time needed?
t = tableWidget->item(i,2);
if (t->text() == "") return;
}
// if you got here then all entries that need a date have a date
abortButton->setDisabled(false);
}
// the code here is tedious. checking for conditions about total ride time
// and if it fits in a day, then distributing them over the day, but
// only if the time is not set, and then if its being set to today
// then start must be before the current time cause you can't log
// data for future rides ... and so on.
//
// but then this feature is gonna save a lot of typing over time ...
//
void
RideImportWizard::todayClicked(int index)
{
QDate selectedDate; // the date we're gonna apply to the row(s) highlighted
// set the index back to 0, so we can select again
todayButton->setCurrentIndex(0);
// 0 = nothing selected, 1 = Today - 2 = last monday thru to 8 = last sunday
if (index == 0) { // no selection
return;
} else if (index == 1) { // today
selectedDate = QDate().currentDate();
} else if (index == 9) { // other date - set focus on first highlighted date
for (int i=0; i<filenames.count(); i++) {
if (tableWidget->item(i,0)->isSelected() ||
tableWidget->item(i,1)->isSelected() ||
tableWidget->item(i,2)->isSelected() ||
tableWidget->item(i,3)->isSelected() ||
tableWidget->item(i,4)->isSelected() ||
tableWidget->item(i,5)->isSelected()) {
tableWidget->editItem(tableWidget->item(i,1));
return;
}
}
return;
} else { // last mon/tue/wed/thu/fri/sat/sun
int daysago = (QDate().currentDate().dayOfWeek() - index + 8) % 7;
if (!daysago) daysago=7; // e.g.last wednesday is 7 days ago if today is wednesday
selectedDate = QDate().fromJulianDay(QDate().currentDate().toJulianDay()-daysago);
}
// Only apply to selected date - set time to current time - ride duration
// pretty daft but at least it sets it to something, anything is gonna be random
int countselected = 0;
int totalduration = 0;
for (int i=0; i< filenames.count(); i++) {
if (tableWidget->item(i,0)->isSelected() ||
tableWidget->item(i,1)->isSelected() ||
tableWidget->item(i,2)->isSelected() ||
tableWidget->item(i,3)->isSelected() ||
tableWidget->item(i,4)->isSelected() ||
tableWidget->item(i,5)->isSelected()) {
countselected++;
QTime duration = QTime().fromString(tableWidget->item(i,3)->text(), "hh:mm:ss");
totalduration += duration.hour() * 3600 +
duration.minute() * 60 +
duration.second();
}
}
// More than a days worth of rides so can't squeeze into a single day!
if (totalduration > (24 * 3600)) {
QMessageBox::warning ( this, tr ( "Invalid Selection" ), tr ( "More than 24hrs of activities to fit into a day" ));
return;
}
// if it is today then start from now - total rides duration
// if that goes negative then just do the std thing
int rstart=0;
if (index == 1) {
QTime timenow = QTime().currentTime();
int now = timenow.hour() * 3600 +
timenow.minute() * 60 +
timenow.second();
rstart = now - totalduration;
}
if (rstart <= 0) { // i.e. still not set properly...
// if it is not being set to "today" then spread them across the
// day back to back with equal time before and after noon.
rstart = ((24*3600) - totalduration) /2;
}
// zip through the rows and where highlighted set the date
// if the start time is not set set it to rstart and increment
// by the duration of the ride.
for (int i=0; i< filenames.count(); i++) {
if (tableWidget->item(i,0)->isSelected() ||
tableWidget->item(i,1)->isSelected() ||
tableWidget->item(i,2)->isSelected() ||
tableWidget->item(i,3)->isSelected() ||
tableWidget->item(i,4)->isSelected() ||
tableWidget->item(i,5)->isSelected()) {
// set the date to date selected
tableWidget->item(i,1)->setText(selectedDate.toString(Qt::ISODate));
// look at rides with missing start time - we need to populate those
// ride duration
QTime duration = QTime().fromString(tableWidget->item(i,3)->text(), "hh:mm:ss");
// ride start time
QTime time(rstart/3600, rstart%3600/60, rstart%60);
tableWidget->item(i,2)->setText(time.toString("hh:mm:ss"));
rstart += duration.hour() * 3600 +
duration.minute() * 60 +
duration.second();
}
}
// phew! - repaint!
QApplication::processEvents();
tableWidget->repaint();
}
void
RideImportWizard::cancelClicked()
{
done(0); // you are the weakest link, goodbye.
}
// info structure used by cpi updater
struct cpi_file_info {
QString file, inname, outname;
};
void
RideImportWizard::abortClicked()
{
// NOTE: abort button morphs into save and finish button later - so all 3 variants are processed here
// if done when labelled abort we kill off this dialog
QString label = abortButton->text();
// Process "ABORT"
if (label == tr("Abort")) {
hide();
aborted=true; // terminated. I'll be back.
return;
}
// Process "FINISH"
if (label == tr("Finish")) {
// phew. our work is done. -- lets force an update stats...
hide();
if (autoImportStealth) {
// inform the user that the work is done
QMessageBox::information(NULL, tr("Auto Import"), tr("Automatic import from defined directories is completed."));
}
done(0);
return;
}
// Process "SAVE"
// SAVE STEP 2 - set the labels and make the text un-editable
phaseLabel->setText(tr("Step 4 of 4: Save to Library"));
abortButton->setText(tr("Abort"));
aborted = false;
cancelButton->setHidden(true);
todayButton->setHidden(true);
//overFiles->setHidden(true); // deprecate for this release XXX
// now set this fields uneditable again ... yeesh.
for (int i=0; i <filenames.count(); i++) {
QTableWidgetItem *t = tableWidget->item(i,1);
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
t = tableWidget->item(i,2);
t->setFlags(t->flags() & (~Qt::ItemIsEditable));
}
QChar zero = QLatin1Char ( '0' );
// Saving now - process the files one-by-one
for (int i=0; i< filenames.count(); i++) {
if (tableWidget->item(i,5)->text().startsWith(tr("Error"))) continue; // skip errors
tableWidget->item(i,5)->setText(tr("Saving..."));
tableWidget->setCurrentCell(i,5);
QApplication::processEvents();
if (aborted) { done(0); return; }
this->repaint();
// SAVE STEP 3 - prepare the new file names for the next steps - basic name and .JSON in GC format
QDateTime ridedatetime = QDateTime(QDate().fromString(tableWidget->item(i,1)->text(), Qt::ISODate),
QTime().fromString(tableWidget->item(i,2)->text(), "hh:mm:ss"));
QString targetnosuffix = QString ( "%1_%2_%3_%4_%5_%6" )
.arg ( ridedatetime.date().year(), 4, 10, zero )
.arg ( ridedatetime.date().month(), 2, 10, zero )
.arg ( ridedatetime.date().day(), 2, 10, zero )
.arg ( ridedatetime.time().hour(), 2, 10, zero )
.arg ( ridedatetime.time().minute(), 2, 10, zero )
.arg ( ridedatetime.time().second(), 2, 10, zero );
QString activitiesTarget = QString ("%1.%2" ).arg ( targetnosuffix ).arg ( "json" );
// create filenames incl. directory path for GC .JSON for both /tmpActivities and /activities directory
QString tmpActivitiesFulltarget = tmpActivities.canonicalPath() + "/" + activitiesTarget;
QString finalActivitiesFulltarget = homeActivities.canonicalPath() + "/" + activitiesTarget;
// check if a ride at this point of time already exists in /activities - if yes, skip import
if (QFileInfo(finalActivitiesFulltarget).exists()) { tableWidget->item(i,5)->setText(tr("Error - Activity file exists")); continue; }
// in addition, also check the RideCache for a Ride with the same point in Time in UTC, which also indicates
// that there was already a ride imported - reason is that RideCache start time is in UTC, while the file Name is in "localTime"
// which causes problems when importing the same file (for files which do not have time/date in the file name),
// while the computer has been set to a different time zone
if (context->athlete->rideCache->getRide(ridedatetime.toUTC())) { tableWidget->item(i,5)->setText(tr("Error - Activity file with same start date/time exists")); continue; };
// SAVE STEP 4 - copy the source file to "/imports" directory (if it's not taken from there as source)
// add the date/time of the target to the source file name (for identification)
// copy the sourceFile to /imports ONLY if the source is NOT coming from /imports itself
QFileInfo sourceFileInfo (filenames[i]);
QString importsTarget;
if (sourceFileInfo.canonicalPath() != homeImports.canonicalPath()) {
// add the GC file base name to create unique file names during import
// there should not be 2 ride files with exactly the same time stamp (as this is also not foreseen for the .json)
importsTarget = sourceFileInfo.baseName() + "_" + targetnosuffix + "." + sourceFileInfo.suffix();
QString importsFulltarget = homeImports.canonicalPath() + "/" + importsTarget;
// copy the source file to /imports with adjusted name
QFile source(filenames[i]);
if (!source.copy(importsFulltarget)) {
tableWidget->item(i,5)->setText(tr("Error - copy of %1 to import directory failed").arg(importsTarget));
}
} else {
// file is re-imported from /imports - keep the name for .JSON Source File Tag
importsTarget = sourceFileInfo.fileName();
}
// SAVE STEP 5 - open the file with the respective format reader and export as .JSON
// to track if addRideCache() has caused an error due to bad data we work with a interim directory for the activities
// -- first export to /tmpactivities
// -- second create RideCache() entry
// -- third move file from /tmpactivities to /activities
tableWidget->item(i,5)->setText(tr("Saving file..."));
// serialize the file to .JSON
QStringList errors;
QFile thisfile(filenames[i]);
RideFile *ride(RideFileFactory::instance().openRideFile(context, thisfile, errors));
// did the input file parse ok ? (should be fine here - since it was alrady checked before - but just in case)
if (ride) {
// update ridedatetime and set the Source File name
ride->setStartTime(ridedatetime);
ride->setTag("Source Filename", importsTarget);
ride->setTag("Filename", activitiesTarget);
if (errors.count() > 0)
ride->setTag("Import errors", errors.join("\n"));
// process linked defaults
context->athlete->rideMetadata()->setLinkedDefaults(ride);
// run the processor first... import
DataProcessorFactory::instance().autoProcess(ride, "Auto", "Import");
ride->recalculateDerivedSeries();
// serialize
JsonFileReader reader;
QFile target(tmpActivitiesFulltarget);
if (reader.writeRideFile(context, ride, target)) {
// now try adding the Ride to the RideCache - since this may fail due to various reason, the activity file
// is stored in tmpActivities during this process to understand which file has create the problem when restarting GC
// - only after the step was successful the file is moved
// to the "clean" activities folder
context->athlete->addRide(QFileInfo(tmpActivitiesFulltarget).fileName(),
tableWidget->rowCount() < 20 ? true : false, // don't signal if mass importing
true, true); // file is available only in /tmpActivities, so use this one please
// rideCache is successfully updated, let's move the file to the real /activities
if (moveFile(tmpActivitiesFulltarget, finalActivitiesFulltarget)) {
tableWidget->item(i,5)->setText(tr("File Saved"));
// and correct the path locally stored in Ride Item
context->ride->setFileName(homeActivities.canonicalPath(), activitiesTarget);
} else {
tableWidget->item(i,5)->setText(tr("Error - Moving %1 to activities folder").arg(activitiesTarget));
}
} else {
tableWidget->item(i,5)->setText(tr("Error - .JSON creation failed"));
}
} else {
tableWidget->item(i,5)->setText(tr("Error - Import of activitiy file failed"));
}
// now metrics have been calculated
DataProcessorFactory::instance().autoProcess(ride, "Save", "ADD");
// clear
delete ride;
QApplication::processEvents();
if (aborted) { done(0); return; }
progressBar->setValue(progressBar->value()+1);
this->repaint();
}
// how did we get on in the end then ...
int completed = 0;
for (int i=0; i< filenames.count(); i++)
if (!tableWidget->item(i,5)->text().startsWith(tr("Error"))) {
completed++;
}
tableWidget->setSortingEnabled(true); // so you can browse through errors etc
QString donemessage = QString(tr("Import Complete. %1 of %2 successful."))
.arg(completed, 1, 10, zero)
.arg(filenames.count(), 1, 10, zero);
progressBar->setValue(progressBar->maximum());
phaseLabel->setText(donemessage);
abortButton->setText(tr("Finish"));
aborted = false;
if (autoImportStealth) {
abortClicked(); // simulate pressing the "Finish" button - even if the window got visible
} else {
if (!isActiveWindow()) activateWindow();
}
}
bool
RideImportWizard::moveFile(const QString &source, const QString &target) {
QFile r(source);
// first try it with a rename
if (r.rename(target)) return true; // job is done
// now the harder variant (copy & delete)
if (r.copy(target))
{
// try to remove - but if this fails, no problem, file has been copied at least
r.remove();
// even if remove failed, the copy was successful - so GC is fine
return true;
}
// more required ?
return false;
}
void
RideImportWizard::closeEvent(QCloseEvent* event)
{
if (_importInProcess)
event->ignore();
else
event->accept();
}
void
RideImportWizard::done(int rc)
{
_importInProcess = false;
QDialog::done(rc);
}
// clean up files
RideImportWizard::~RideImportWizard()
{
foreach(QString name, deleteMe) QFile(name).remove();
}
// Below is the code to implement a custom ItemDelegate to support
// editing of the date and time of the Ride inside a QTableWidget
// the ItemDelegate is registered for every cell in the table
// and only kicks into life for the columns in which the date and
// time are stored, and only when the DateTime for that row is 0.
// construct the delegate - save away the column containing the date
// bearing in mind the time is in the next
// column.
RideDelegate::RideDelegate(int dateColumn, QObject *parent) : QItemDelegate(parent)
{
this->dateColumn = dateColumn;
}
// paint the cells for date and time - pass through for all the
// other cells. Uses the model data to display.
void RideDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == dateColumn) {
QString value = index.model()->data(index, Qt::DisplayRole).toString();
// display with angles to show its doing something
QString text = QString("%1").arg(value);
QStyleOptionViewItem myOption = option;
myOption.displayAlignment = Qt::AlignHCenter | Qt::AlignVCenter;
drawDisplay(painter, myOption, myOption.rect, text);
drawFocus(painter, myOption, myOption.rect);
} else if (index.column() == dateColumn+1) {
QString value = index.model()->data(index, Qt::DisplayRole).toString();
// display with angles to show its doing something
QString text = QString("%1").arg(value);
QStyleOptionViewItem myOption = option;
myOption.displayAlignment = Qt::AlignHCenter | Qt::AlignVCenter;
drawDisplay(painter, myOption, myOption.rect, text);
drawFocus(painter, myOption, myOption.rect);
} else {
QItemDelegate::paint(painter, option, index);
}
}
// setup editor for edit of field!!
QWidget *RideDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.column() == dateColumn) {
// edit that date!
QDateEdit *dateEdit = new QDateEdit(parent);
dateEdit->setDisplayFormat("yyyy-MM-dd"); // ISO Format, no translation analog Qt::ISODate
connect(dateEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseDateEditor()));
return dateEdit;
} else if (index.column() == dateColumn+1) {
// edit that time
QTimeEdit *timeEdit = new QTimeEdit(parent);
timeEdit->setDisplayFormat("hh:mm:ss");
connect(timeEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseTimeEditor()));
return timeEdit;
} else {
return QItemDelegate::createEditor(parent, option, index);
}
}
// user hit tab or return so save away the data to our model
void RideDelegate::commitAndCloseDateEditor()
{
QDateEdit *editor = qobject_cast<QDateEdit *>(sender());
emit commitData(editor);
emit closeEditor(editor);
}
// user hit tab or return so save away the data to our model
void RideDelegate::commitAndCloseTimeEditor()
{
QTimeEdit *editor = qobject_cast<QTimeEdit *>(sender());
emit commitData(editor);
emit closeEditor(editor);
}
// We don't set anything because the data is saved within the view not the model!
void RideDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
// stored as text field
if (index.column() == dateColumn) {
QDateEdit *dateEdit = qobject_cast<QDateEdit *>(editor);
QDate date = QDate().fromString(index.model()->data(index, Qt::DisplayRole).toString(), Qt::ISODate);
dateEdit->setDate(date);
} else if (index.column() == dateColumn+1) {
QTimeEdit *timeEdit = qobject_cast<QTimeEdit *>(editor);
QTime time = QTime().fromString(index.model()->data(index, Qt::DisplayRole).toString(), "hh:mm:ss");;
timeEdit->setTime(time);
}
}
// We don't set anything because the data is saved within the view not the model!
void RideDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
// stored as text field
if (index.column() == dateColumn) {
QDateEdit *dateEdit = qobject_cast<QDateEdit *>(editor);
QString value = dateEdit->date().toString(Qt::ISODate);
// Place in the view
model->setData(index, value, Qt::DisplayRole);
} else if (index.column() == dateColumn+1) {
QTimeEdit *timeEdit = qobject_cast<QTimeEdit *>(editor);
QString value = timeEdit->time().toString("hh:mm:ss");
model->setData(index, value, Qt::DisplayRole);
}
}