mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
When reading a PWX file we try to guess the recording interval by looking at the first two samples. This can lead to silly values when there are recording errors. This patch sanity checks the recording interval and sets it to 1s recording if the derived value is unusual.
546 lines
20 KiB
C++
546 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();
|
|
// 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.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("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");
|
|
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);
|
|
}
|