Files
GoldenCheetah/src/FileIO/TcxParser.cpp
2020-03-29 22:41:48 -03:00

442 lines
18 KiB
C++

/*
* Copyright (c) 2008 Sean C. Rhea (srhea@srhea.net),
* J.T Conklin (jtc@acorntoolworks.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 <QString>
#include <QDebug>
#include "TcxParser.h"
#include "TimeUtils.h"
// use stc strtod to bypass Qt toDouble() issues
#include <stdlib.h>
// TCX XML Structure uses the following 2 Schema Definitions
// -- main schema http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd
// -- extension schema http://www8.garmin.com/xmlschemas/ActivityExtensionv2.xsd
TcxParser::TcxParser (RideFile* rideFile, QList<RideFile*> *rides) : rideFile(rideFile), rides(rides)
{
isGarminSmartRecording = appsettings->value(NULL, GC_GARMIN_SMARTRECORD,Qt::Checked);
GarminHWM = appsettings->value(NULL, GC_GARMIN_HWMARK);
if (GarminHWM.isNull() || GarminHWM.toInt() == 0) GarminHWM.setValue(25); // default to 25 seconds.
first = true;
creator = false;
}
bool
TcxParser::startElement( const QString&, const QString&, const QString& qName, const QXmlAttributes& qAttributes)
{
buffer.clear();
if (qName == "Activity") {
// First initialisation for altitude (not initialised for each point)
alt= 0;
// length-by-length pool swimming XData
swimXdata = new XDataSeries();
swimXdata->name = "SWIM";
swimXdata->valuename << "TYPE";
swimXdata->valuename << "DURATION";
lap = 0;
for (int i = 0; i < ltLast; ++i)
lapCount[i] = 0;
if (first == true) first = false;
else {
rideFile = new RideFile();
rideFile->setRecIntSecs(1.0);
rideFile->setDeviceType("Garmin");
rideFile->setFileFormat("Garmin Training Centre (tcx)");
}
// if caller is looking for rides...
if (rides) rides->append(rideFile);
// Sport ("Biking", "Running", "Other")
swim = NotSwim;
QString sport = qAttributes.value("Sport");
if (sport == "Biking") rideFile->setTag("Sport", "Bike");
else if (sport == "Running") rideFile->setTag("Sport", "Run");
else if (sport == "Other") swim = MayBeSwim;
// start of last length for lap swimming
lastLength = 0.0;
} else if (qName == "Lap") {
lap_start_time = convertToLocalTime(qAttributes.value("StartTime").trimmed());
lapSecs = 0.0;
lapTrigger = ltManual;
// Use the time of the first lap as the time of the activity.
if (lap == 0) {
start_time = lap_start_time;
rideFile->setStartTime(start_time);
last_distance = 0.0;
last_time = start_time;
}
lap++;
} else if (qName == "Trackpoint") {
power = 0.0;
cadence = 0.0;
rcad = 0.0;
speed = 0.0;
headwind = 0.0;
torque = 0;
hr = 0.0;
lat = 0.0;
lon = 0.0;
lrbalance = RideFile::NA;
lte = 0.0;
rte = 0.0;
lps = 0.0;
rps = 0.0;
badgps = false;
//alt = 0.0; // TCX from FIT files have not alt point for each trackpoint
distance = -1; // nh - we set this to -1 so we can detect if there was a distance in the trackpoint.
secs = 0;
} else if (qName == "Creator") {
creator = true;
}
return true;
}
bool
TcxParser::endElement( const QString&, const QString&, const QString& qName)
{
if (qName == "Time") {
time = convertToLocalTime(buffer);
secs = double(start_time.msecsTo(time)) / 1000.00f;
} else if (qName == "DistanceMeters") { distance = buffer.toDouble() / 1000; }
else if (qName == "TotalTimeSeconds") { lapSecs = buffer.toDouble(); }
else if (qName == "Watts" || qName.endsWith( ":Watts")) { power = buffer.toDouble(); } //TCX Extension Fields may use a namespace prefix
else if (qName == "Speed" || qName.endsWith(":Speed")) { speed = buffer.toDouble() * 3.6; } //TCX Extension Fields may use a namespace prefix
else if (qName == "RunCadence" || qName.endsWith( ":RunCadence")) { rcad = buffer.toDouble(); } //TCX Extension Fields may use a namespace prefix
else if (qName == "Value") { hr = buffer.toDouble(); }
else if (qName == "Cadence") { cadence = buffer.toDouble(); }
else if (qName == "PedalPower") { lrbalance = buffer.toDouble(); }
else if (qName == "TorqueEffLeft") { lte = buffer.toDouble(); }
else if (qName == "TorqueEffRight") { rte = buffer.toDouble(); }
else if (qName == "PedalSmoothLeft") { lps = buffer.toDouble(); }
else if (qName == "PedalSmoothRight") { rps = buffer.toDouble(); }
else if (qName == "AltitudeMeters") {
// on Suunto TCX files there are lots of 0 values between valid ones, skip these
if (buffer.toDouble() != 0) {
alt = buffer.toDouble();
}
} else if (qName == "LongitudeDegrees") {
char *p;
setlocale(LC_NUMERIC,"C"); // strtod is locale dependent!
lon = strtod(buffer.toLatin1(), &p);
setlocale(LC_NUMERIC,"");
} else if (qName == "LatitudeDegrees") {
char *p;
setlocale(LC_NUMERIC,"C"); // strtod is locale dependent!
lat = strtod(buffer.toLatin1(), &p);
setlocale(LC_NUMERIC,"");
} else if (qName == "Trackpoint") {
// Some TCX lap swimming files uses distance = 0 for no distance...
if (swim == Swim && distance == 0) distance = -1;
// Some TCX files have Speed, some have Distance
// Lets derive Speed from Distance or vice-versa
// If we have neither Speed nor Distance then we
// add a point with 0 for speed and distance
double sample_dist = distance;
if (speed == 0 || distance < 0) {
// compute the elapsed time and distance traveled since the
// last recorded trackpoint
double delta_t = double(last_time.msecsTo(time)) / 1000.0f;
// Derive speed from distance
if (speed == 0 && distance >0) {
double delta_d = distance - last_distance;
if (delta_d<0) delta_d=0;
if (delta_t > 0.0) speed=delta_d / delta_t * 3600.0;
} else if (distance < 0) { // otherwise derive distance from speed
double delta_d = delta_t * speed / 3600.0;
distance = last_distance + delta_d;
}
}
// Record trackpoint
if (lat == 0 && lon == 0) badgps = true;
// If sport was Other and we have distance but no GPS data
// we assume it is a pool swimming activity and first
// distance is pool length
if (swim == MayBeSwim && badgps && distance > 0) {
swim = Swim;
rideFile->setTag("Sport", "Swim");
rideFile->setTag("Pool Length", // in meters
QString("%1").arg(distance*1000.0));
}
// for smart recording, the delta_t will not be constant
// average all the calculations based on the previous
// point.
if(rideFile->dataPoints().empty()) {
// first point
rideFile->appendPoint(secs, cadence, hr, distance, speed, torque,
power, alt, lon, lat, headwind, 0.0,
RideFile::NA, lrbalance,
lte,rte,lps,rps,
0.0,0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0,rcad,0.0, // no running dynamics in the schema ?
0.0, //tcore
lap);
} else {
// assumption that the change in ride is linear... :)
RideFilePoint *prevPoint = rideFile->dataPoints().back();
double deltaSecs = secs - prevPoint->secs;
double deltaCad = cadence - prevPoint->cad;
double deltaHr = hr - prevPoint->hr;
double deltaDist = distance - prevPoint->km;
double deltaSpeed = speed - prevPoint->kph;
double deltaTorque = torque - prevPoint->nm;
double deltaPower = power - prevPoint->watts;
double deltaAlt = alt - prevPoint->alt;
double deltaLon = lon - prevPoint->lon;
double deltaLat = lat - prevPoint->lat;
double deltarcad = rcad - prevPoint->rcad;
double deltaLrbalance = lrbalance - prevPoint->lrbalance;
double deltaLte = lte - prevPoint->lte;
double deltaRte = rte - prevPoint->rte;
double deltaLps = lps - prevPoint->lps;
double deltaRps = rps - prevPoint->rps;
if (prevPoint->lat == 0 && prevPoint->lon == 0) badgps = true;
// Smart Recording High Water Mark.
if ((isGarminSmartRecording.toInt() == 0) || (deltaSecs == 1) || (deltaSecs >= GarminHWM.toInt() && swim != Swim)) {
// no smart recording, or delta exceeds HW treshold, just insert the data
rideFile->appendPoint(secs, cadence, hr, distance, speed, torque, power,
alt, lon, lat, headwind, 0.0, RideFile::NA, lrbalance,
lte,rte,lps,rps,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0,0.0,
0.0, // vertical oscillation
rcad, // run cadence
0.0, // gct
0.0, // tcore
lap);
// Update distance and speed for swimming lengths
if (swim == Swim && sample_dist > 0.0 && secs > lastLength) {
double deltaSecs = secs - lastLength;
double deltaDist = (distance - last_distance) / deltaSecs;
double kph = 3600.0 * deltaDist;
// length-by-length Swim XData
XDataPoint *p = new XDataPoint();
p->secs = lastLength;
p->km = last_distance;
p->number[0] = deltaDist > 0 ? 1 : 0;
p->number[1] = deltaSecs;
if (swimXdata) swimXdata->datapoints.append(p);
for (int i = rideFile->timeIndex(lastLength);
i>= 0 && i < rideFile->dataPoints().size() &&
rideFile->dataPoints()[i]->secs <= secs;
++i) {
rideFile->dataPoints()[i]->kph = kph;
rideFile->dataPoints()[i]->km = last_distance;
last_distance += deltaDist;
}
last_distance = distance;
if (kph > 0.0) rideFile->setDataPresent(rideFile->kph, true);
lastLength = secs;
}
} else {
// smart recording is on and delta is less than GarminHWM seconds
// length-by-length Swim XData
if (swim == Swim && deltaSecs > 0) {
XDataPoint *p = new XDataPoint();
p->secs = prevPoint->secs;
p->km = last_distance;
p->number[0] = deltaDist > 0 ? 1 : 0;
p->number[1] = deltaSecs;
if (swimXdata) swimXdata->datapoints.append(p);
lastLength = p->secs + deltaSecs;
}
// or it is pool swimming and we limit expansion for safety
for(int i = 1; i <= deltaSecs && i <= 300*GarminHWM.toInt(); i++) {
double weight = i/ deltaSecs;
double kph = (swim == Swim) ? speed : prevPoint->kph + (deltaSpeed *weight);
// need to make sure speed goes to zero
kph = kph > 0.35 ? kph : 0;
//double cad = prevPoint->cad + (deltaCad * weight);
//cad = cad > 0.35 ? cad : 0;
//double lat = prevPoint->lat + (deltaLat * weight);
//double lon = prevPoint->lon + (deltaLon * weight);
rideFile->appendPoint(prevPoint->secs + (deltaSecs * weight),
prevPoint->cad + (deltaCad * weight),
prevPoint->hr + (deltaHr * weight),
prevPoint->km + (deltaDist * weight),
kph,
prevPoint->nm + (deltaTorque * weight),
prevPoint->watts + (deltaPower * weight),
prevPoint->alt + (deltaAlt * weight),
badgps ? 0 : prevPoint->lon + (deltaLon * weight), // lon
badgps ? 0 : prevPoint->lat + (deltaLat * weight), // lat
headwind, // headwind
0.0, // slope
RideFile::NA,
prevPoint->lrbalance + (deltaLrbalance * weight),
prevPoint->lte + (deltaLte * weight),
prevPoint->rte + (deltaRte * weight),
prevPoint->lps + (deltaLps * weight),
prevPoint->rps + (deltaRps * weight),
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0,0.0,
0.0, // vertical oscillation
prevPoint->rcad + (deltarcad * weight),// run cadence
0.0, // gct
0.0, //tcore
lap);
}
prevPoint = rideFile->dataPoints().back();
}
}
last_distance = distance;
last_time = time;
} else if (qName == "TriggerMethod") {
// see "TriggerMethod_t" in Garmin's Training Center Database XML (TCX) Schema
if (buffer == "Distance")
lapTrigger = ltDistance;
else if (buffer == "Location")
lapTrigger = ltLocation;
else if (buffer == "Time")
lapTrigger = ltTime;
else if (buffer == "HeartRate")
lapTrigger = ltHeartRate;
} else if (qName == "Lap") {
// for pool swimming, laps with distance 0 are pauses, without trackpoints
// length-by-length Swim XData
if (swim == Swim && distance == 0.0) {
XDataPoint *p = new XDataPoint();
p->secs = secs;
p->km = last_distance;
p->number[0] = 0;
p->number[1] = round(lapSecs);
if (swimXdata) swimXdata->datapoints.append(p);
lastLength = secs + round(lapSecs);
}
// expand even if Smart Recording is not enabled
if (swim == Swim && distance == 0) {
// fill in the pause, partially if too long
for(int i = 1; i <= round(lapSecs) && i <= 300*GarminHWM.toInt(); i++)
rideFile->appendPoint(secs + i,
0.0,
0.0,
last_distance,
0.0,
0.0,
0.0,
0.0,
0.0, // lon
0.0, // lat
0.0, // headwind
0.0,
RideFile::NA,
0.0, // rlbalance
0.0,0.0,0.0,0.0, // lte, rte, lps, rps
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0,0.0,
0.0, // vertical oscillation
0.0, // run cadence
0.0, // gct
0.0, // tcore
lap);
last_time = last_time.addSecs(round(lapSecs));
}
QString name;
switch (lapTrigger) {
case ltDistance:
name = QObject::tr("Distance %1").arg(++lapCount[lapTrigger]);
break;
case ltLocation:
name = QObject::tr("Location %1").arg(++lapCount[lapTrigger]);
break;
case ltTime:
name = QObject::tr("Time %1").arg(++lapCount[lapTrigger]);
break;
case ltHeartRate:
name = QObject::tr("HeartRate %1").arg(++lapCount[lapTrigger]);
break;
default:
name = QObject::tr("Lap %1").arg(++lapCount[ltManual]);
break;
}
double start = double(start_time.msecsTo(lap_start_time)) / 1000.00f;
rideFile->addInterval(RideFileInterval::DEVICE, start, start + lapSecs, name);
} else if (qName == "Activity") {
// Add length-by-length Swim XData, if present
if (swimXdata && swimXdata->datapoints.count()>0) {
rideFile->addXData("SWIM", swimXdata);
} else if (swimXdata) {
delete swimXdata;
swimXdata = NULL;
}
} else if (qName == "Notes") {
// Add Notes to metadata
rideFile->setTag("Notes", buffer);
} else if (qName == "Creator") {
creator = false;
} else if (creator && qName == "Name") {
if (!buffer.isEmpty())
rideFile->setDeviceType(buffer);
}
return true;
}
bool TcxParser::characters( const QString& str )
{
buffer += str;
return true;
}