mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
.. mostly comments about features not yet implemented, but not relevant to current feature set anyway.
551 lines
20 KiB
C++
551 lines
20 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 "PwxRideFile.h"
|
|
#include "Settings.h"
|
|
#include <QDomDocument>
|
|
#include <QVector>
|
|
#include <assert.h>
|
|
|
|
#include <QDebug>
|
|
|
|
static int pwxFileReaderRegistered =
|
|
RideFileFactory::instance().registerReader(
|
|
"pwx", "TrainingPeaks PWX", new PwxFileReader());
|
|
|
|
RideFile *
|
|
PwxFileReader::openRideFile(QFile &file, QStringList &errors, QList<RideFile*>*) const
|
|
{
|
|
QDomDocument doc("TrainingPeaks 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);
|
|
rideFile->setFileFormat("Peaksware Data File (pwx)");
|
|
|
|
// 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 = duration.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();
|
|
// NOTE! undo the fudge to set zero values to
|
|
// 1 in the writer (below). This is to keep
|
|
// the TP upload web-service happy with zero values
|
|
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.slope, add.temp, add.lrbalance, 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);
|
|
|
|
// if its a daft number then make it 1s -- there is probably
|
|
// a gap in recording in there.
|
|
switch ((int)rideFile->recIntSecs()) {
|
|
case 1 : // lots!
|
|
case 4 : // garmin smart recording
|
|
case 5 : // polar sometimes
|
|
case 10 : // polar and others
|
|
case 15 :
|
|
break;
|
|
|
|
default:
|
|
rideFile->setRecIntSecs(1);
|
|
break;
|
|
}
|
|
}
|
|
return rideFile;
|
|
}
|
|
|
|
bool
|
|
PwxFileReader::writeRideFile(MainWindow *main, 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("creator", "Golden Cheetah");
|
|
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(main->cyclist); name.appendChild(text);
|
|
athlete.appendChild(name);
|
|
double cyclistweight = ride->getTag("Weight", "0.0").toDouble();
|
|
if (cyclistweight) {
|
|
QDomElement weight = doc.createElement("weight");
|
|
text = doc.createTextNode(QString("%1").arg(cyclistweight));
|
|
weight.appendChild(text);
|
|
athlete.appendChild(weight);
|
|
}
|
|
root.appendChild(athlete);
|
|
|
|
// sport
|
|
QString sport = ride->getTag("Sport", "Bike");
|
|
if (sport == QObject::tr("Biking") || sport == QObject::tr("Cycling") || sport == QObject::tr("Cycle") || sport == QObject::tr("Bike")) {
|
|
sport = "Bike";
|
|
}
|
|
QDomElement sportType = doc.createElement("sportType");
|
|
text = doc.createTextNode(sport);
|
|
sportType.appendChild(text);
|
|
root.appendChild(sportType);
|
|
|
|
// notes
|
|
if (ride->getTag("Notes","") != "") {
|
|
QDomElement notes = doc.createElement("cmt");
|
|
text = doc.createTextNode(ride->getTag("Notes","")); notes.appendChild(text);
|
|
root.appendChild(notes);
|
|
}
|
|
|
|
|
|
// 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);
|
|
}
|
|
|
|
|
|
// 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);
|
|
}
|