mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
All are harmless but for the sake of clarity have fixed them all bar a couple; - Lucene grumbles about signed/unsigned conversion which is/isn't valid depending upon the version of CLucene you compile with. Either way it is harmless. - QxtScheduleView has a bunch of issues, but since it is a third party widget its better to leave it unchanged.
433 lines
17 KiB
C++
433 lines
17 KiB
C++
/*
|
|
* Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
|
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "TxtRideFile.h"
|
|
#include "Units.h"
|
|
#include <QRegExp>
|
|
#include <QTextStream>
|
|
#include <QVector>
|
|
#include <QDebug>
|
|
#include <algorithm> // for std::sort
|
|
#include <assert.h>
|
|
#include "math.h"
|
|
|
|
static int txtFileReaderRegistered =
|
|
RideFileFactory::instance().registerReader(
|
|
"txt","Racermate/Ergvideo/Wattbike", new TxtFileReader());
|
|
|
|
// Racermate Workout Data Export File Format (.TXT)
|
|
// ------------------------------------------------
|
|
//
|
|
// The Racermate TXT format export of CDF format files is also
|
|
// used by ErgVideo 3. It is rather similar to the .ERG and .CRS
|
|
// file formats
|
|
//
|
|
// The sample values can be comma separated or space separated
|
|
// depending upon configuration settings in CompCS, indeed the
|
|
// fields that are present are also configurable, which is why
|
|
// there is a line prior to the sample data that lists the
|
|
// field names.
|
|
//
|
|
// There are two example files in the test/rides directory which
|
|
// can be used as a reference for the file format.
|
|
|
|
RideFile *TxtFileReader::openRideFile(QFile &file, QStringList &errors, QList<RideFile*>*) const
|
|
{
|
|
QString deviceInfo; // keep a record of all the data in the
|
|
// sections so we can store it in the device
|
|
// info metadata field
|
|
|
|
QString section = ""; // the text for the section we are currently in
|
|
// if it is blank we are not in any section, and should
|
|
// expect to see a section, or 'number of records = ' or
|
|
// column headings or raw data
|
|
|
|
QStringList headings; // the headings array
|
|
int timeIndex = -1;
|
|
int cadIndex = -1;
|
|
int hrIndex = -1;
|
|
int kmIndex = -1;
|
|
int kphIndex = -1;
|
|
int milesIndex = -1;
|
|
int wattsIndex = -1;
|
|
int headwindIndex = -1;
|
|
|
|
bool metric = true; // are the values in metric or imperial?
|
|
QDateTime startTime; // parsed from filename
|
|
|
|
// just to quieten the compiler, since we don't seem to
|
|
// use the metric bool anywhere in the code at present
|
|
if (metric) { }
|
|
|
|
// Lets make sure we can open the file
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
errors << ("Could not open ride file: \"" + file.fileName() + "\"");
|
|
return NULL;
|
|
}
|
|
QTextStream in(&file);
|
|
|
|
// Now we need to determine if this is a Wattbike export
|
|
// or a Racermate export. We can do this by looking at
|
|
// the first line of the file.
|
|
//
|
|
// For it to be a wattbike, the first line is made up
|
|
// of multiple tokens, separated by a tab, which match
|
|
// the pattern "name [units]"
|
|
bool isWattBike = false;
|
|
QStringList tokens = in.readLine().split(QRegExp("[\t\n\r]"), QString::SkipEmptyParts);
|
|
|
|
if (tokens.count() > 1) {
|
|
// ok, so we have a bunch of tokens, thats a good sign this
|
|
// may be a wattbike export, now are all the tokens of the
|
|
// form "name [units]", or just "Index" or "name []"
|
|
isWattBike=true;
|
|
QRegExp wattToken("^[^[]+ \\[[^]]+\\]$");
|
|
foreach(QString token, tokens) {
|
|
if (wattToken.exactMatch(token) != true) {
|
|
QRegExp noUnits("^[^[]+ \\[\\]$");
|
|
if (token != "Index" && !noUnits.exactMatch(token)) {
|
|
isWattBike = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isWattBike) {
|
|
|
|
// RACERMATE STYLE
|
|
|
|
file.close(); // start again (seek did weird things on Linux, bug (?)
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
errors << ("Could not open ride file: \"" + file.fileName() + "\"");
|
|
return NULL;
|
|
}
|
|
QTextStream is(&file);
|
|
|
|
// Lets construct our rideFile
|
|
RideFile *rideFile = new RideFile();
|
|
rideFile->setDeviceType("Computrainer/Velotron");
|
|
rideFile->setFileFormat("Computrainer/Velotron text file (txt)");
|
|
|
|
while (!is.atEnd()) {
|
|
|
|
// the readLine() method doesn't handle old Macintosh CR line endings
|
|
// this workaround will load the the entire file if it has CR endings
|
|
// then split and loop through each line
|
|
// otherwise, there will be nothing to split and it will read each line as expected.
|
|
QString linesIn = is.readLine();
|
|
QStringList lines = linesIn.split('\r');
|
|
if(lines.isEmpty()) continue;
|
|
|
|
// loop through the lines we got
|
|
foreach (QString line, lines) {
|
|
|
|
QRegExp sectionPattern("^\\[.*\\]$");
|
|
QRegExp unitsPattern("^UNITS += +\\(.*\\)$");
|
|
QRegExp sepPattern("( +|,)");
|
|
|
|
// ignore blank lines
|
|
if (line == "") continue;
|
|
|
|
// begin or end of section
|
|
if (sectionPattern.exactMatch(line)) {
|
|
deviceInfo += line;
|
|
deviceInfo += "\n";
|
|
|
|
if (section == "") section = line;
|
|
else section = "";
|
|
continue;
|
|
}
|
|
|
|
// section Data
|
|
if (section != "") {
|
|
// save it away
|
|
deviceInfo += line;
|
|
deviceInfo += "\n";
|
|
|
|
// look for UNITS line
|
|
if (unitsPattern.exactMatch(line)) {
|
|
if (unitsPattern.cap(1) != "METRIC") metric = false;
|
|
else metric = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// number of records, jsut ignore it
|
|
if (line.startsWith("number of")) continue;
|
|
|
|
// either a data line, or a headings line
|
|
if (headings.count() == 0) {
|
|
headings = line.split(sepPattern, QString::SkipEmptyParts);
|
|
|
|
// where are the values stored?
|
|
timeIndex = headings.indexOf("ms");
|
|
wattsIndex = headings.indexOf("watts");
|
|
cadIndex = headings.indexOf("rpm");
|
|
hrIndex = headings.indexOf("hr");
|
|
kmIndex = headings.indexOf("KM");
|
|
milesIndex = headings.indexOf("miles");
|
|
kphIndex = headings.indexOf("speed");
|
|
headwindIndex = headings.indexOf("wind");
|
|
continue;
|
|
}
|
|
// right! we now have a record
|
|
QStringList values = line.split(sepPattern, QString::SkipEmptyParts);
|
|
|
|
// mmm... didn't get much data
|
|
if (values.count() < 2) continue;
|
|
|
|
// extract out each value. Remove double quotes if they are there (Newer Racermate TXT files)
|
|
double secs = timeIndex > -1 ? values[timeIndex].remove("\"").toDouble() / (double) 1000 : 0.0;
|
|
double watts = wattsIndex > -1 ? values[wattsIndex].remove("\"").toDouble() : 0.0;
|
|
double cad = cadIndex > -1 ? values[cadIndex].remove("\"").toDouble() : 0.0;
|
|
double hr = hrIndex > -1 ? values[hrIndex].remove("\"").toDouble() : 0.0;
|
|
double km = kmIndex > -1 ? values[kmIndex].remove("\"").toDouble() : 0.0;
|
|
double kph = kphIndex > -1 ? values[kphIndex].remove("\"").toDouble() : 0.0;
|
|
double miles = milesIndex > -1 ? values[milesIndex].remove("\"").toDouble() : 0.0;
|
|
double headwind = headwindIndex > -1 ? values[headwindIndex].remove("\"").toDouble() : 0.0;
|
|
if (miles != 0) {
|
|
// imperial!
|
|
kph *= KM_PER_MILE;
|
|
km = miles * KM_PER_MILE;
|
|
}
|
|
rideFile->appendPoint(secs, cad, hr, km, kph, 0.0, watts, 0.0, 0.0, 0.0, headwind, 0.0, RideFile::noTemp, 0.0, 0);
|
|
|
|
}
|
|
}
|
|
file.close();
|
|
|
|
rideFile->setTag("Device Info", deviceInfo);
|
|
|
|
//
|
|
// To estimate the recording interval, take the median of the
|
|
// first 1000 samples and round to nearest millisecond.
|
|
//
|
|
int n = rideFile->dataPoints().size();
|
|
n = qMin(n, 1000);
|
|
if (n >= 2) {
|
|
|
|
QVector<double> secs(n-1);
|
|
for (int i = 0; i < n-1; ++i) {
|
|
double now = rideFile->dataPoints()[i]->secs;
|
|
double then = rideFile->dataPoints()[i+1]->secs;
|
|
secs[i] = then - now;
|
|
}
|
|
std::sort(secs.begin(), secs.end());
|
|
int mid = n / 2 - 1;
|
|
double recint = round(secs[mid] * 1000.0) / 1000.0;
|
|
rideFile->setRecIntSecs(recint);
|
|
|
|
} else {
|
|
|
|
// less than 2 data points is not a valid ride file
|
|
errors << "Insufficient valid data in file \"" + file.fileName() + "\".";
|
|
delete rideFile;
|
|
file.close();
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// Get date time from standard GC name
|
|
//
|
|
QRegExp gcPattern("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.txt$");
|
|
gcPattern.setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
// Racemermate uses name-mode-yyyy-mm-dd-hh-mm-ss.CDF.txt
|
|
QRegExp rmPattern("^.*/[^-]*-[^-]*-(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)-(\\d\\d)-(\\d\\d)-(\\d\\d)\\.cdf.txt$");
|
|
rmPattern.setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
// Ergvideo uses name_ergvideo_ridename_yyyy-mm-dd@hh-mm-ss.txt
|
|
QRegExp evPattern("^.*/[^_]*_[^_]*_[^_]*_(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)@(\\d\\d)-(\\d\\d)-(\\d\\d)\\.txt$");
|
|
evPattern.setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
// It is a GC Filename
|
|
if (gcPattern.exactMatch(file.fileName())) {
|
|
QDateTime datetime(QDate(gcPattern.cap(1).toInt(),
|
|
gcPattern.cap(2).toInt(),
|
|
gcPattern.cap(3).toInt()),
|
|
QTime(gcPattern.cap(4).toInt(),
|
|
gcPattern.cap(5).toInt(),
|
|
gcPattern.cap(6).toInt()));
|
|
rideFile->setStartTime(datetime);
|
|
}
|
|
|
|
// It is an Ergvideo Filename
|
|
if (evPattern.exactMatch(file.fileName())) {
|
|
QDateTime datetime(QDate(evPattern.cap(1).toInt(),
|
|
evPattern.cap(2).toInt(),
|
|
evPattern.cap(3).toInt()),
|
|
QTime(evPattern.cap(4).toInt(),
|
|
evPattern.cap(5).toInt(),
|
|
evPattern.cap(6).toInt()));
|
|
rideFile->setStartTime(datetime);
|
|
}
|
|
|
|
// It is an Racermate Filename
|
|
if (rmPattern.exactMatch(file.fileName())) {
|
|
QDateTime datetime(QDate(rmPattern.cap(1).toInt(),
|
|
rmPattern.cap(2).toInt(),
|
|
rmPattern.cap(3).toInt()),
|
|
QTime(rmPattern.cap(4).toInt(),
|
|
rmPattern.cap(5).toInt(),
|
|
rmPattern.cap(6).toInt()));
|
|
rideFile->setStartTime(datetime);
|
|
}
|
|
|
|
return rideFile;
|
|
|
|
} else {
|
|
// WATTBIKE STYLE
|
|
|
|
// Lets construct our rideFile
|
|
RideFile *rideFile = new RideFile();
|
|
rideFile->setDeviceType("Wattbike");
|
|
rideFile->setFileFormat("Wattbike text file (txt)");
|
|
rideFile->setRecIntSecs(1);
|
|
|
|
// We need to work out which column represents
|
|
// which data series. The heading tokens specify
|
|
// both the series and units, but for now we will
|
|
// assume the units are standardised until we have
|
|
// seen files with different values for units.
|
|
|
|
// Here are the known heading tokens:
|
|
// "Elapsed time total [s]"
|
|
// "Cadence [1/min]"
|
|
// "Velocity [km/h]"
|
|
// "Distance total [m]"
|
|
// "Heart rate [1/min]"
|
|
// "Torque per revolution [Nm]"
|
|
// "Power per revolution [W]"
|
|
//
|
|
// Note: no "spinscan" type data is provided.
|
|
|
|
int timeIndex = -1;
|
|
int kmIndex = -1;
|
|
int rpmIndex = -1;
|
|
int kphIndex = -1;
|
|
int bpmIndex = -1;
|
|
int torqIndex = -1;
|
|
int wattsIndex = -1;
|
|
|
|
// lets initialise the indexes for all the values
|
|
QRegExp wattToken("^([^[]+) \\[([^]]+)\\]$");
|
|
int i=0;
|
|
int columns = tokens.count();
|
|
|
|
foreach(QString token, tokens) {
|
|
|
|
if (wattToken.exactMatch(token)) {
|
|
QString name = wattToken.cap(1);
|
|
QString unit = wattToken.cap(2);
|
|
|
|
if (name == "Elapsed time total") timeIndex = i;
|
|
if (name == "Distance total") kmIndex = i;
|
|
if (name == "Cadence") rpmIndex = i;
|
|
if (name == "Velocity") kphIndex = i;
|
|
if (name == "Heart rate") bpmIndex = i;
|
|
if (name == "Torque per revolution") torqIndex = i;
|
|
if (name == "Power per revolution") wattsIndex = i;
|
|
|
|
}
|
|
i++;
|
|
}
|
|
|
|
// lets loop through each row of data adding a sample
|
|
// using the indexes we set above
|
|
double rsecs = 0;
|
|
while (!in.atEnd()) {
|
|
|
|
QString line = in.readLine();
|
|
QStringList tokens = line.split(QRegExp("[\r\n\t]"), QString::SkipEmptyParts);
|
|
|
|
// do we have as many columns as we expected?
|
|
if (tokens.count() == columns) {
|
|
|
|
double secs = 0.00f;
|
|
double km = 0.00f;
|
|
double rpm = 0.00f;
|
|
double kph = 0.00f;
|
|
double bpm = 0.00f;
|
|
double torq = 0.00f;
|
|
double watts = 0.00f;
|
|
|
|
if (timeIndex >= 0) {
|
|
QTime time = QTime::fromString(tokens.at(timeIndex), "mm:ss:00");
|
|
secs = QTime::fromString("00:00:00", "mm:ss:00").secsTo(time);
|
|
|
|
// its a bit shit, but the format appears to wrap round
|
|
// on the hour; 59:59:00 is followed by 00:00:00
|
|
// so we have a problem, since if there are gaps in
|
|
// recording then we don't know which hour this is for
|
|
// and if we assume largely contiguous data we may as
|
|
// well just use a counter instead.
|
|
//
|
|
// for expediency, we use a counter for now:
|
|
secs = rsecs++;
|
|
}
|
|
if (kmIndex >= 0) km = tokens.at(kmIndex).toDouble() / 1000;
|
|
if (rpmIndex >= 0) rpm = tokens.at(rpmIndex).toDouble();
|
|
if (kphIndex >= 0) kph = tokens.at(kphIndex).toDouble();
|
|
if (bpmIndex >= 0) bpm = tokens.at(bpmIndex).toDouble();
|
|
if (torqIndex >= 0) torq = tokens.at(torqIndex).toDouble();
|
|
if (wattsIndex >= 0) watts = tokens.at(wattsIndex).toDouble();
|
|
|
|
rideFile->appendPoint(secs, rpm, bpm, km, kph, torq, watts, 0.0, 0.0, 0.0, 0.0, 0.0, RideFile::noTemp, 0.0, 0);
|
|
}
|
|
}
|
|
|
|
QRegExp gcPattern("^.*/(\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)\\.txt$");
|
|
gcPattern.setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
// Only filename we have seen is name_yyymmdd or name_yyymmdd_all.txt
|
|
QRegExp wb1Pattern("^.*/[^_]*_(\\d\\d\\d\\d)(\\d\\d)(\\d\\d).txt$");
|
|
wb1Pattern.setCaseSensitivity(Qt::CaseInsensitive);
|
|
QRegExp wb2Pattern("^.*/[^_]*_(\\d\\d\\d\\d)(\\d\\d)(\\d\\d)_all.txt$");
|
|
wb2Pattern.setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
// It is a GC Filename
|
|
if (gcPattern.exactMatch(file.fileName())) {
|
|
QDateTime datetime(QDate(gcPattern.cap(1).toInt(),
|
|
gcPattern.cap(2).toInt(),
|
|
gcPattern.cap(3).toInt()),
|
|
QTime(gcPattern.cap(4).toInt(),
|
|
gcPattern.cap(5).toInt(),
|
|
gcPattern.cap(6).toInt()));
|
|
rideFile->setStartTime(datetime);
|
|
}
|
|
|
|
// It is an Wattbike Filename
|
|
if (wb1Pattern.exactMatch(file.fileName())) {
|
|
QDateTime datetime(QDate(wb1Pattern.cap(1).toInt(),
|
|
wb1Pattern.cap(2).toInt(),
|
|
wb1Pattern.cap(3).toInt()),
|
|
QTime(0, 0, 0));
|
|
rideFile->setStartTime(datetime);
|
|
}
|
|
if (wb2Pattern.exactMatch(file.fileName())) {
|
|
QDateTime datetime(QDate(wb2Pattern.cap(1).toInt(),
|
|
wb2Pattern.cap(2).toInt(),
|
|
wb2Pattern.cap(3).toInt()),
|
|
QTime(0, 0, 0));
|
|
rideFile->setStartTime(datetime);
|
|
}
|
|
|
|
return rideFile;
|
|
}
|
|
}
|