mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
PWX Ride file support
Support for Training peaks new .pwx file format. This is an XML format (and is particularly verbose). Support has been added to enable interoperability with WKO+ v3, TrainingPeaks.com and Device Agent.
This commit is contained in:
515
src/PwxRideFile.cpp
Normal file
515
src/PwxRideFile.cpp
Normal file
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
* 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 "PwxRideFile.h"
|
||||
#include "Settings.h"
|
||||
#include <QDomDocument>
|
||||
#include <QVector>
|
||||
#include <assert.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
static int pwxFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader(
|
||||
"pwx", "Training Peaks PWX", new PwxFileReader());
|
||||
|
||||
RideFile *
|
||||
PwxFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
{
|
||||
QDomDocument doc("Training Peaks PWX");
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
errors << "Could not open file.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool parsed = doc.setContent(&file);
|
||||
file.close();
|
||||
if (!parsed) {
|
||||
errors << "Could not parse file.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PwxFromDomDoc(doc, errors);
|
||||
}
|
||||
|
||||
RideFile *
|
||||
PwxFileReader::PwxFromDomDoc(QDomDocument doc, QStringList &errors) const
|
||||
{
|
||||
RideFile *rideFile = new RideFile();
|
||||
QDomElement root = doc.documentElement();
|
||||
QDomNode workout = root.firstChildElement("workout");
|
||||
QDomNode node = workout.firstChild();
|
||||
|
||||
// can arrive at any time, so lets cache them
|
||||
// and sort out at the end
|
||||
QDateTime rideDate;
|
||||
QString rideNotes;
|
||||
|
||||
int intervals = 0;
|
||||
int samples = 0;
|
||||
|
||||
// in case we need to calculate distance
|
||||
double rtime = 0;
|
||||
double rdist = 0;
|
||||
|
||||
while (!node.isNull()) {
|
||||
|
||||
// athlete
|
||||
if (node.nodeName() == "athlete") {
|
||||
|
||||
QDomElement name = node.firstChildElement("name");
|
||||
if (!name.isNull()) {
|
||||
rideFile->setTag("Athlete Name", name.text());
|
||||
}
|
||||
|
||||
QDomElement weight = node.firstChildElement("weight");
|
||||
if (!weight.isNull()) {
|
||||
rideFile->setTag("Weight", weight.text());
|
||||
}
|
||||
|
||||
// workout code
|
||||
} else if (node.nodeName() == "code") {
|
||||
|
||||
QDomElement code = node.toElement();
|
||||
rideFile->setTag("Workout Code", code.text());
|
||||
|
||||
// goal / objective
|
||||
} else if (node.nodeName() == "goal") {
|
||||
|
||||
QDomElement goal = node.toElement();
|
||||
rideFile->setTag("Objective", goal.text());
|
||||
|
||||
// sport
|
||||
} else if (node.nodeName() == "sportType") {
|
||||
|
||||
QDomElement sport = node.toElement();
|
||||
rideFile->setTag("Sport", sport.text());
|
||||
|
||||
// notes
|
||||
} else if (node.nodeName() == "cmt") {
|
||||
|
||||
QDomElement notes = node.toElement();
|
||||
rideNotes = notes.text();
|
||||
|
||||
// device type and info
|
||||
} else if (node.nodeName() == "device") {
|
||||
|
||||
QString devicetype;
|
||||
// make and model
|
||||
QDomElement make = node.firstChildElement("make");
|
||||
if (!make.isNull()) devicetype = make.text();
|
||||
QDomElement model = node.firstChildElement("model");
|
||||
if (!model.isNull()) {
|
||||
if (devicetype != "") devicetype += " ";
|
||||
devicetype += model.text();
|
||||
}
|
||||
rideFile->setDeviceType(devicetype);
|
||||
|
||||
// device settings data
|
||||
QString deviceinfo;
|
||||
QDomElement extension = node.firstChildElement("extension");
|
||||
if (!extension.isNull()) {
|
||||
for (QDomElement info=extension.firstChildElement();
|
||||
!info.isNull();
|
||||
info = info.nextSiblingElement()) {
|
||||
deviceinfo += info.tagName();
|
||||
deviceinfo += ": ";
|
||||
deviceinfo += info.text();
|
||||
deviceinfo += '\n';
|
||||
}
|
||||
}
|
||||
rideFile->setTag("Device Info", deviceinfo);
|
||||
|
||||
// start date/time
|
||||
} else if (node.nodeName() == "time") {
|
||||
QDomElement date = node.toElement();
|
||||
rideDate = QDateTime::fromString(date.text(), Qt::ISODate);
|
||||
rideFile->setStartTime(rideDate);
|
||||
|
||||
// interval data
|
||||
} else if (node.nodeName() == "segment") {
|
||||
RideFileInterval add;
|
||||
|
||||
// name
|
||||
QDomElement name = node.firstChildElement("name");
|
||||
if (!name.isNull()) add.name = name.text();
|
||||
else add.name = QString("Interval #%1").arg(++intervals);
|
||||
|
||||
QDomElement summary = node.firstChildElement("summarydata");
|
||||
if (!summary.isNull()) {
|
||||
|
||||
// start
|
||||
QDomElement beginning = summary.firstChildElement("beginning");
|
||||
if (!beginning.isNull()) add.start = beginning.text().toDouble();
|
||||
else add.start = -1;
|
||||
|
||||
// duration - convert to end
|
||||
QDomElement duration = summary.firstChildElement("duration");
|
||||
if (!duration.isNull() && add.start != -1)
|
||||
add.stop = beginning.text().toDouble() + add.start;
|
||||
else
|
||||
add.stop = -1;
|
||||
|
||||
// add interval
|
||||
if (add.start != -1 && add.stop != -1) {
|
||||
rideFile->addInterval(add.start, add.stop, add.name);
|
||||
}
|
||||
}
|
||||
|
||||
// data points: offset, hr, spd, pwr, torq, cad, dist, lat, lon, alt (ignored: temp, time)
|
||||
} else if (node.nodeName() == "sample") {
|
||||
RideFilePoint add;
|
||||
|
||||
// offset (secs)
|
||||
QDomElement off = node.firstChildElement("timeoffset");
|
||||
if (!off.isNull()) add.secs = off.text().toDouble();
|
||||
else add.secs = 0.0;
|
||||
// hr
|
||||
QDomElement hr = node.firstChildElement("hr");
|
||||
if (!hr.isNull()) add.hr = hr.text().toDouble();
|
||||
else add.hr = 0.0;
|
||||
// spd in meters per second converted to kph
|
||||
QDomElement spd = node.firstChildElement("spd");
|
||||
if (!spd.isNull()) add.kph = spd.text().toDouble() * 3.6;
|
||||
else add.kph = 0.0;
|
||||
// pwr
|
||||
QDomElement pwr = node.firstChildElement("pwr");
|
||||
if (!pwr.isNull()) {
|
||||
add.watts = pwr.text().toDouble();
|
||||
// XXX undo the fudge to set zero values to
|
||||
// 1 in the writer (below). This is to keep
|
||||
// the TP upload web-service happy
|
||||
if (add.watts == 1) add.watts = 0.0;
|
||||
} else add.watts = 0.0;
|
||||
// torq
|
||||
QDomElement torq = node.firstChildElement("torq");
|
||||
if (!torq.isNull()) add.nm = torq.text().toDouble();
|
||||
else add.nm = 0.0;
|
||||
// cad
|
||||
QDomElement cad = node.firstChildElement("cad");
|
||||
if (!cad.isNull()) add.cad = cad.text().toDouble();
|
||||
else add.cad = 0.0;
|
||||
// dist
|
||||
QDomElement dist = node.firstChildElement("dist");
|
||||
if (!dist.isNull()) add.km = dist.text().toDouble() /1000;
|
||||
else add.km = 0.0;
|
||||
|
||||
// lat
|
||||
QDomElement lat = node.firstChildElement("lat");
|
||||
if (!lat.isNull()) add.lat = lat.text().toDouble();
|
||||
else add.lat = 0.0;
|
||||
// lon
|
||||
QDomElement lon = node.firstChildElement("lon");
|
||||
if (!lon.isNull()) add.lon = lon.text().toDouble();
|
||||
else add.lon = 0.0;
|
||||
// alt
|
||||
QDomElement alt = node.firstChildElement("alt");
|
||||
if (!alt.isNull()) add.alt = alt.text().toDouble();
|
||||
else add.alt = 0.0;
|
||||
|
||||
// do we need to calculate distance?
|
||||
if (add.km == 0.0 && samples) {
|
||||
// delta secs * kph/3600
|
||||
add.km = rdist + ((add.secs - rtime) * (add.kph/3600));
|
||||
}
|
||||
|
||||
// running totals
|
||||
samples++;
|
||||
rtime = add.secs;
|
||||
rdist = add.km;
|
||||
|
||||
// add the data point
|
||||
rideFile->appendPoint(add.secs, add.cad, add.hr, add.km, add.kph,
|
||||
add.nm, add.watts, add.alt, add.lon, add.lat, add.headwind, add.interval);
|
||||
|
||||
|
||||
// ignored for now
|
||||
} else if (node.nodeName() == "summarydata") {
|
||||
} else if (node.nodeName() == "extension") {
|
||||
}
|
||||
|
||||
node = node.nextSibling();
|
||||
}
|
||||
|
||||
// post-process and check
|
||||
if (samples < 2) {
|
||||
errors << "Not enough samples present";
|
||||
delete rideFile;
|
||||
return NULL;
|
||||
} else {
|
||||
// need to determine the recIntSecs - first - second sample?
|
||||
rideFile->setRecIntSecs(rideFile->dataPoints()[1]->secs -
|
||||
rideFile->dataPoints()[0]->secs);
|
||||
}
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
bool
|
||||
PwxFileReader::writeRideFile(const QString cyclist, const RideFile *ride, QFile &file) const
|
||||
{
|
||||
QDomText text; // used all over
|
||||
QDomDocument doc;
|
||||
QDomProcessingInstruction hdr = doc.createProcessingInstruction("xml","version=\"1.0\"");
|
||||
doc.appendChild(hdr);
|
||||
|
||||
// pwx
|
||||
QDomElement pwx = doc.createElementNS("http://www.peaksware.com/PWX/1/0", "pwx");
|
||||
pwx.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
|
||||
pwx.setAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
|
||||
pwx.setAttribute("xsi:schemaLocation", "http://www.peaksware.com/PWX/1/0 http://www.peaksware.com/PWX/1/0/pwx.xsd");
|
||||
pwx.setAttribute("version", "1.0");
|
||||
doc.appendChild(pwx);
|
||||
|
||||
// workouts... we just serialise 1 at a time
|
||||
QDomElement root = doc.createElement("workout");
|
||||
pwx.appendChild(root);
|
||||
|
||||
// athlete details
|
||||
QDomElement athlete = doc.createElement("athlete");
|
||||
QDomElement name = doc.createElement("name");
|
||||
text = doc.createTextNode(cyclist); name.appendChild(text);
|
||||
athlete.appendChild(name);
|
||||
root.appendChild(athlete);
|
||||
|
||||
// sport
|
||||
QString sport = ride->getTag("Sport", "Bike");
|
||||
QDomElement sportType = doc.createElement("sportType");
|
||||
text = doc.createTextNode(sport); sportType.appendChild(text);
|
||||
root.appendChild(sportType);
|
||||
|
||||
// workout code
|
||||
if (ride->getTag("Workout Code", "") != "") {
|
||||
QString wcode = ride->getTag("Workout Code", "");
|
||||
QDomElement code = doc.createElement("code");
|
||||
text = doc.createTextNode(wcode); code.appendChild(text);
|
||||
root.appendChild(code);
|
||||
}
|
||||
|
||||
// goal
|
||||
if (ride->getTag("Objective", "") != "") {
|
||||
QString obj = ride->getTag("Objective", "");
|
||||
QDomElement goal = doc.createElement("goal");
|
||||
text = doc.createTextNode(obj); goal.appendChild(text);
|
||||
root.appendChild(goal);
|
||||
}
|
||||
|
||||
|
||||
// notes
|
||||
// XXX todo
|
||||
// device type
|
||||
if (ride->deviceType() != "") {
|
||||
QDomElement device = doc.createElement("device");
|
||||
device.setAttribute("id", ride->deviceType());
|
||||
QDomElement make = doc.createElement("make");
|
||||
text = doc.createTextNode(ride->deviceType());
|
||||
make.appendChild(text);
|
||||
device.appendChild(make);
|
||||
root.appendChild(device);
|
||||
}
|
||||
|
||||
// time
|
||||
QDomElement time = doc.createElement("time");
|
||||
text = doc.createTextNode(ride->startTime().toString(Qt::ISODate));
|
||||
time.appendChild(text);
|
||||
root.appendChild(time);
|
||||
|
||||
// summary data
|
||||
QDomElement summarydata = doc.createElement("summarydata");
|
||||
root.appendChild(summarydata);
|
||||
QDomElement beginning = doc.createElement("beginning");
|
||||
text = doc.createTextNode(QString("%1").arg(ride->dataPoints().first()->secs));
|
||||
beginning.appendChild(text);
|
||||
summarydata.appendChild(beginning);
|
||||
|
||||
QDomElement duration = doc.createElement("duration");
|
||||
text = doc.createTextNode(QString("%1").arg(ride->dataPoints().last()->secs));
|
||||
duration.appendChild(text);
|
||||
summarydata.appendChild(duration);
|
||||
|
||||
// the channels - min max avg get set by TP anyway
|
||||
// so we leave them blank to save time on calculating them
|
||||
if (ride->areDataPresent()->hr) {
|
||||
QDomElement s = doc.createElement("hr");
|
||||
s.setAttribute("max", "0");
|
||||
s.setAttribute("min", "0");
|
||||
s.setAttribute("avg", "0");
|
||||
summarydata.appendChild(s);
|
||||
}
|
||||
if (ride->areDataPresent()->kph) {
|
||||
QDomElement s = doc.createElement("spd");
|
||||
s.setAttribute("max", "0");
|
||||
s.setAttribute("min", "0");
|
||||
s.setAttribute("avg", "0");
|
||||
summarydata.appendChild(s);
|
||||
}
|
||||
if (ride->areDataPresent()->watts) {
|
||||
QDomElement s = doc.createElement("pwr");
|
||||
s.setAttribute("max", "0");
|
||||
s.setAttribute("min", "0");
|
||||
s.setAttribute("avg", "0");
|
||||
summarydata.appendChild(s);
|
||||
}
|
||||
if (ride->areDataPresent()->nm) {
|
||||
QDomElement s = doc.createElement("torq");
|
||||
s.setAttribute("max", "0");
|
||||
s.setAttribute("min", "0");
|
||||
s.setAttribute("avg", "0");
|
||||
summarydata.appendChild(s);
|
||||
}
|
||||
if (ride->areDataPresent()->cad) {
|
||||
QDomElement s = doc.createElement("cad");
|
||||
s.setAttribute("max", "0");
|
||||
s.setAttribute("min", "0");
|
||||
s.setAttribute("avg", "0");
|
||||
summarydata.appendChild(s);
|
||||
}
|
||||
QDomElement dist = doc.createElement("dist");
|
||||
text = doc.createTextNode(QString("%1").arg((int)(ride->dataPoints().last()->km * 1000)));
|
||||
dist.appendChild(text);
|
||||
summarydata.appendChild(dist);
|
||||
|
||||
if (ride->areDataPresent()->alt) {
|
||||
QDomElement s = doc.createElement("alt");
|
||||
s.setAttribute("max", "0");
|
||||
s.setAttribute("min", "0");
|
||||
s.setAttribute("avg", "0");
|
||||
summarydata.appendChild(s);
|
||||
}
|
||||
|
||||
// interval "segments"
|
||||
if (!ride->intervals().empty()) {
|
||||
foreach (RideFileInterval i, ride->intervals()) {
|
||||
QDomElement segment = doc.createElement("segment");
|
||||
root.appendChild(segment);
|
||||
|
||||
// name
|
||||
QDomElement name = doc.createElement("name");
|
||||
text = doc.createTextNode(i.name); name.appendChild(text);
|
||||
segment.appendChild(name);
|
||||
|
||||
// summarydata
|
||||
QDomElement summarydata = doc.createElement("summarydata");
|
||||
segment.appendChild(summarydata);
|
||||
|
||||
// beginning
|
||||
QDomElement beginning = doc.createElement("beginning");
|
||||
text = doc.createTextNode(QString("%1").arg(i.start));
|
||||
beginning.appendChild(text);
|
||||
summarydata.appendChild(beginning);
|
||||
|
||||
// duration
|
||||
QDomElement duration = doc.createElement("duration");
|
||||
text = doc.createTextNode(QString("%1").arg(i.stop - i.start));
|
||||
duration.appendChild(text);
|
||||
summarydata.appendChild(duration);
|
||||
}
|
||||
}
|
||||
|
||||
// samples
|
||||
// data points: timeoffset, dist, hr, spd, pwr, torq, cad, lat, lon, alt
|
||||
if (!ride->dataPoints().empty()) {
|
||||
foreach (const RideFilePoint *point, ride->dataPoints()) {
|
||||
QDomElement sample = doc.createElement("sample");
|
||||
root.appendChild(sample);
|
||||
|
||||
// time
|
||||
QDomElement timeoffset = doc.createElement("timeoffset");
|
||||
text = doc.createTextNode(QString("%1").arg((int)point->secs));
|
||||
timeoffset.appendChild(text);
|
||||
sample.appendChild(timeoffset);
|
||||
|
||||
// hr
|
||||
if (ride->areDataPresent()->hr) {
|
||||
QDomElement hr = doc.createElement("hr");
|
||||
text = doc.createTextNode(QString("%1").arg((int)point->hr));
|
||||
hr.appendChild(text);
|
||||
sample.appendChild(hr);
|
||||
}
|
||||
// spd - meters per second
|
||||
if (ride->areDataPresent()->kph) {
|
||||
QDomElement spd = doc.createElement("spd");
|
||||
text = doc.createTextNode(QString("%1").arg(point->kph / 3.6));
|
||||
spd.appendChild(text);
|
||||
sample.appendChild(spd);
|
||||
}
|
||||
// pwr
|
||||
if (ride->areDataPresent()->watts) {
|
||||
// TrainingPeaks.com file upload rejects rides
|
||||
// with excessive power of zero for some reason
|
||||
// looks like they expect some smoothing or something?
|
||||
// we set 0 to 1 to at least get an upload
|
||||
// and do the reverse in the reader above
|
||||
int watts = point->watts ? point->watts : 1;
|
||||
QDomElement pwr = doc.createElement("pwr");
|
||||
text = doc.createTextNode(QString("%1").arg(watts));
|
||||
pwr.appendChild(text);
|
||||
sample.appendChild(pwr);
|
||||
}
|
||||
// torq
|
||||
if (ride->areDataPresent()->nm) {
|
||||
QDomElement torq = doc.createElement("torq");
|
||||
text = doc.createTextNode(QString("%1").arg(point->nm));
|
||||
torq.appendChild(text);
|
||||
sample.appendChild(torq);
|
||||
}
|
||||
// cad
|
||||
if (ride->areDataPresent()->cad) {
|
||||
QDomElement cad = doc.createElement("cad");
|
||||
text = doc.createTextNode(QString("%1").arg((int)(point->cad)));
|
||||
cad.appendChild(text);
|
||||
sample.appendChild(cad);
|
||||
}
|
||||
|
||||
// distance - meters
|
||||
QDomElement dist = doc.createElement("dist");
|
||||
text = doc.createTextNode(QString("%1").arg((int)(point->km*1000)));
|
||||
dist.appendChild(text);
|
||||
sample.appendChild(dist);
|
||||
|
||||
|
||||
// lat
|
||||
if (ride->areDataPresent()->lat && point->lat > -90.0 && point->lat < 90.0) {
|
||||
QDomElement lat = doc.createElement("lat");
|
||||
text = doc.createTextNode(QString("%1").arg(point->lat));
|
||||
lat.appendChild(text);
|
||||
sample.appendChild(lat);
|
||||
}
|
||||
// lon
|
||||
if (ride->areDataPresent()->lon && point->lon > -180.00 && point->lon < 180.00) {
|
||||
QDomElement lon = doc.createElement("lon");
|
||||
text = doc.createTextNode(QString("%1").arg(point->lon));
|
||||
lon.appendChild(text);
|
||||
sample.appendChild(lon);
|
||||
}
|
||||
|
||||
// alt
|
||||
if (ride->areDataPresent()->alt) {
|
||||
QDomElement alt = doc.createElement("alt");
|
||||
text = doc.createTextNode(QString("%1").arg(point->alt));
|
||||
alt.appendChild(text);
|
||||
sample.appendChild(alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray xml = doc.toByteArray(4);
|
||||
if (!file.open(QIODevice::WriteOnly)) return(false);
|
||||
if (file.write(xml) != xml.size()) return(false);
|
||||
file.close();
|
||||
return(true);
|
||||
}
|
||||
Reference in New Issue
Block a user