mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
Add a TCX ride exporter
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include "Colors.h"
|
||||
#include "ConfigDialog.h"
|
||||
#include "PwxRideFile.h"
|
||||
#include "TcxRideFile.h"
|
||||
#include "GcRideFile.h"
|
||||
#include "JsonRideFile.h"
|
||||
#ifdef GC_HAVE_KML
|
||||
@@ -486,6 +487,7 @@ MainWindow::MainWindow(const QDir &home) :
|
||||
rideMenu->addAction(tr("&Export to KML..."), this, SLOT(exportKML()));
|
||||
#endif
|
||||
rideMenu->addAction(tr("Export to PWX..."), this, SLOT(exportPWX()));
|
||||
rideMenu->addAction(tr("Export to TCX..."), this, SLOT(exportTCX()));
|
||||
#ifdef GC_HAVE_SOAP
|
||||
rideMenu->addSeparator ();
|
||||
rideMenu->addAction(tr("&Export Metrics as CSV..."), this, SLOT(exportMetrics()), tr(""));
|
||||
@@ -1128,6 +1130,26 @@ MainWindow::exportPWX()
|
||||
reader.writeRideFile(cyclist, currentRide(), file);
|
||||
}
|
||||
|
||||
void
|
||||
MainWindow::exportTCX()
|
||||
{
|
||||
if ((treeWidget->selectedItems().size() != 1)
|
||||
|| (treeWidget->selectedItems().first()->type() != RIDE_TYPE)) {
|
||||
QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString fileName = QFileDialog::getSaveFileName(
|
||||
this, tr("Export TCX"), QDir::homePath(), tr("TCX (*.tcx)"));
|
||||
if (fileName.length() == 0)
|
||||
return;
|
||||
|
||||
QString err;
|
||||
QFile file(fileName);
|
||||
TcxFileReader reader;
|
||||
reader.writeRideFile(this, cyclist, currentRide(), file);
|
||||
}
|
||||
|
||||
#ifdef GC_HAVE_KML
|
||||
void
|
||||
MainWindow::exportKML()
|
||||
|
||||
@@ -182,6 +182,7 @@ class MainWindow : public QMainWindow
|
||||
void downloadRide();
|
||||
void manualRide();
|
||||
void exportPWX();
|
||||
void exportTCX();
|
||||
void exportCSV();
|
||||
void exportGC();
|
||||
void exportJson();
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
|
||||
#include "TcxRideFile.h"
|
||||
#include "TcxParser.h"
|
||||
#include <QDomDocument>
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "RideMetric.h"
|
||||
|
||||
#ifndef GC_VERSION
|
||||
#define GC_VERSION "(developer build)"
|
||||
#endif
|
||||
|
||||
static int tcxFileReaderRegistered =
|
||||
RideFileFactory::instance().registerReader(
|
||||
@@ -40,3 +48,272 @@ RideFile *TcxFileReader::openRideFile(QFile &file, QStringList &errors) const
|
||||
|
||||
return rideFile;
|
||||
}
|
||||
|
||||
bool
|
||||
TcxFileReader::writeRideFile(MainWindow *mainWindow, const QString cyclist, const RideFile *ride, QFile &file) const
|
||||
{
|
||||
QDomText text;
|
||||
QDomDocument doc;
|
||||
QDomProcessingInstruction hdr = doc.createProcessingInstruction("xml","version=\"1.0\"");
|
||||
doc.appendChild(hdr);
|
||||
|
||||
// pwx
|
||||
QDomElement tcx = doc.createElementNS("http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2", "TrainingCenterDatabase");
|
||||
tcx.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
|
||||
tcx.setAttribute("xsi:schemaLocation", "http://www.garmin.com/xmlschemas/ActivityExtension/v2 http://www.garmin.com/xmlschemas/ActivityExtensionv2.xsd http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd");
|
||||
//tcx.setAttribute("version", "2.0");
|
||||
doc.appendChild(tcx);
|
||||
|
||||
// activities, we just serialise one ride
|
||||
QDomElement activities = doc.createElement("Activities");
|
||||
tcx.appendChild(activities);
|
||||
QDomElement activity = doc.createElement("Activity");
|
||||
activity.setAttribute("Sport", "Biking"); // was ride->getTag("Sport", "Biking") but must be Biking, Running or Other
|
||||
activities.appendChild(activity);
|
||||
|
||||
// time
|
||||
QDomElement id = doc.createElement("Id");
|
||||
text = doc.createTextNode(ride->startTime().toString(Qt::ISODate)+"Z");
|
||||
id.appendChild(text);
|
||||
activity.appendChild(id);
|
||||
|
||||
QDomElement lap = doc.createElement("Lap");
|
||||
lap.setAttribute("StartTime", ride->startTime().toString(Qt::ISODate)+"Z");
|
||||
activity.appendChild(lap);
|
||||
|
||||
const char *metrics[] = {
|
||||
"total_distance",
|
||||
"workout_time",
|
||||
"total_work",
|
||||
NULL
|
||||
};
|
||||
|
||||
QStringList worklist = QStringList();
|
||||
for (int i=0; metrics[i];i++) worklist << metrics[i];
|
||||
|
||||
QHash<QString, RideMetricPtr> computed = RideMetric::computeMetrics(mainWindow, ride, mainWindow->zones(), mainWindow->hrZones(), worklist);
|
||||
|
||||
QDomElement lap_time = doc.createElement("TotalTimeSeconds");
|
||||
text = doc.createTextNode(QString("%1").arg(computed.value("workout_time")->value(true)));
|
||||
//text = doc.createTextNode(ride->dataPoints().last()->secs);
|
||||
lap_time.appendChild(text);
|
||||
lap.appendChild(lap_time);
|
||||
|
||||
QDomElement lap_distance = doc.createElement("DistanceMeters");
|
||||
text = doc.createTextNode(QString("%1").arg(1000*computed.value("total_distance")->value(true)));
|
||||
//text = doc.createTextNode(ride->dataPoints().last()->km);
|
||||
lap_distance.appendChild(text);
|
||||
lap.appendChild(lap_distance);
|
||||
|
||||
QDomElement lap_calories = doc.createElement("Calories");
|
||||
text = doc.createTextNode(QString("%1").arg((int)computed.value("total_work")->value(true)));
|
||||
lap_calories.appendChild(text);
|
||||
lap.appendChild(lap_calories);
|
||||
|
||||
QDomElement lap_intensity = doc.createElement("Intensity");
|
||||
text = doc.createTextNode("Active");
|
||||
lap_intensity.appendChild(text);
|
||||
lap.appendChild(lap_intensity);
|
||||
|
||||
QDomElement lap_triggerMethod = doc.createElement("TriggerMethod");
|
||||
text = doc.createTextNode("Manual");
|
||||
lap_triggerMethod.appendChild(text);
|
||||
lap.appendChild(lap_triggerMethod);
|
||||
|
||||
|
||||
// samples
|
||||
// data points: timeoffset, dist, hr, spd, pwr, torq, cad, lat, lon, alt
|
||||
if (!ride->dataPoints().empty()) {
|
||||
QDomElement track = doc.createElement("Track");
|
||||
lap.appendChild(track);
|
||||
|
||||
foreach (const RideFilePoint *point, ride->dataPoints()) {
|
||||
QDomElement trackpoint = doc.createElement("Trackpoint");
|
||||
track.appendChild(trackpoint);
|
||||
|
||||
// time
|
||||
QDomElement time = doc.createElement("Time");
|
||||
text = doc.createTextNode(ride->startTime().addSecs(point->secs).toString(Qt::ISODate)+"Z");
|
||||
time.appendChild(text);
|
||||
trackpoint.appendChild(time);
|
||||
|
||||
// position
|
||||
if (ride->areDataPresent()->lat && point->lat > -90.0 && point->lat < 90.0 && point->lat != 0.0 &&
|
||||
ride->areDataPresent()->lon && point->lon > -180.00 && point->lon < 180.00 && point->lon != 0.0 ) {
|
||||
QDomElement position = doc.createElement("Position");
|
||||
trackpoint.appendChild(position);
|
||||
|
||||
// lat
|
||||
QDomElement lat = doc.createElement("LatitudeDegrees");
|
||||
text = doc.createTextNode(QString("%1").arg(point->lat));
|
||||
lat.appendChild(text);
|
||||
position.appendChild(lat);
|
||||
|
||||
// lon
|
||||
QDomElement lon = doc.createElement("LongitudeDegrees");
|
||||
text = doc.createTextNode(QString("%1").arg(point->lon));
|
||||
lon.appendChild(text);
|
||||
position.appendChild(lon);
|
||||
}
|
||||
|
||||
// alt
|
||||
if (ride->areDataPresent()->alt && point->alt != 0.0) {
|
||||
QDomElement alt = doc.createElement("AltitudeMeters");
|
||||
text = doc.createTextNode(QString("%1").arg(point->alt));
|
||||
alt.appendChild(text);
|
||||
trackpoint.appendChild(alt);
|
||||
}
|
||||
|
||||
// distance - meters
|
||||
if (ride->areDataPresent()->km) {
|
||||
QDomElement dist = doc.createElement("DistanceMeters");
|
||||
text = doc.createTextNode(QString("%1").arg((int)(point->km*1000)));
|
||||
dist.appendChild(text);
|
||||
trackpoint.appendChild(dist);
|
||||
}
|
||||
|
||||
// hr
|
||||
if (ride->areDataPresent()->hr) {
|
||||
QDomElement hr = doc.createElement("HeartRateBpm");
|
||||
hr.setAttribute("xsi:type", "HeartRateInBeatsPerMinute_t");
|
||||
QDomElement value = doc.createElement("Value");
|
||||
text = doc.createTextNode(QString("%1").arg((int)point->hr));
|
||||
value.appendChild(text);
|
||||
hr.appendChild(value);
|
||||
trackpoint.appendChild(hr);
|
||||
}
|
||||
|
||||
// cad
|
||||
if (ride->areDataPresent()->cad && point->cad < 255) { //xsd maxInclusive value="254"
|
||||
QDomElement cad = doc.createElement("Cadence");
|
||||
text = doc.createTextNode(QString("%1").arg((int)(point->cad)));
|
||||
cad.appendChild(text);
|
||||
trackpoint.appendChild(cad);
|
||||
}
|
||||
|
||||
if (ride->areDataPresent()->kph || ride->areDataPresent()->watts) {
|
||||
QDomElement extension = doc.createElement("Extensions");
|
||||
trackpoint.appendChild(extension);
|
||||
QDomElement tpx = doc.createElement("TPX");
|
||||
tpx.setAttribute("xmlns", "http://www.garmin.com/xmlschemas/ActivityExtension/v2");
|
||||
extension.appendChild(tpx);
|
||||
|
||||
// spd - meters per second
|
||||
if (ride->areDataPresent()->kph) {
|
||||
QDomElement spd = doc.createElement("Speed");
|
||||
text = doc.createTextNode(QString("%1").arg(point->kph / 3.6));
|
||||
spd.appendChild(text);
|
||||
tpx.appendChild(spd);
|
||||
}
|
||||
// pwr
|
||||
if (ride->areDataPresent()->watts) {
|
||||
QDomElement pwr = doc.createElement("Watts");
|
||||
text = doc.createTextNode(QString("%1").arg(point->watts));
|
||||
pwr.appendChild(text);
|
||||
tpx.appendChild(pwr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creator - Device
|
||||
QDomElement creator = doc.createElement("Creator");
|
||||
creator.setAttribute("xsi:type", "Device_t");
|
||||
activity.appendChild(creator);
|
||||
|
||||
QDomElement creator_name = doc.createElement("Name");
|
||||
if (ride->deviceType() != "")
|
||||
text = doc.createTextNode(ride->deviceType());
|
||||
else
|
||||
text = doc.createTextNode("Unknow");
|
||||
creator_name.appendChild(text);
|
||||
creator.appendChild(creator_name);
|
||||
|
||||
QDomElement creator_unitId = doc.createElement("UnitId");
|
||||
text = doc.createTextNode("0");
|
||||
creator_unitId.appendChild(text);
|
||||
creator.appendChild(creator_unitId);
|
||||
|
||||
QDomElement creator_productId = doc.createElement("ProductId");
|
||||
text = doc.createTextNode("0");
|
||||
creator_productId.appendChild(text);
|
||||
creator.appendChild(creator_productId);
|
||||
|
||||
|
||||
QDomElement creator_version = doc.createElement("Version");
|
||||
creator.appendChild(creator_version);
|
||||
|
||||
QDomElement creator_version_version_major = doc.createElement("VersionMajor");
|
||||
text = doc.createTextNode("3");
|
||||
creator_version_version_major.appendChild(text);
|
||||
creator_version.appendChild(creator_version_version_major);
|
||||
|
||||
QDomElement creator_version_version_minor = doc.createElement("VersionMinor");
|
||||
text = doc.createTextNode("0");
|
||||
creator_version_version_minor.appendChild(text);
|
||||
creator_version.appendChild(creator_version_version_minor);
|
||||
|
||||
QDomElement creator_version_build_major = doc.createElement("BuildMajor");
|
||||
text = doc.createTextNode("0");
|
||||
creator_version_build_major.appendChild(text);
|
||||
creator_version.appendChild(creator_version_build_major);
|
||||
|
||||
QDomElement creator_version_build_minor = doc.createElement("BuildMinor");
|
||||
text = doc.createTextNode("0");
|
||||
creator_version_build_minor.appendChild(text);
|
||||
creator_version.appendChild(creator_version_build_minor);
|
||||
|
||||
|
||||
// Author - Application
|
||||
QDomElement author = doc.createElement("Author");
|
||||
author.setAttribute("xsi:type", "Application_t");
|
||||
tcx.appendChild(author);
|
||||
|
||||
QDomElement author_name = doc.createElement("Name");
|
||||
text = doc.createTextNode("GoldenCheetah");
|
||||
author_name.appendChild(text);
|
||||
author.appendChild(author_name);
|
||||
|
||||
QDomElement author_build = doc.createElement("Build");
|
||||
author.appendChild(author_build);
|
||||
|
||||
QDomElement author_version = doc.createElement("Version");
|
||||
author_build.appendChild(author_version);
|
||||
|
||||
QDomElement author_version_version_major = doc.createElement("VersionMajor");
|
||||
text = doc.createTextNode("3");
|
||||
author_version_version_major.appendChild(text);
|
||||
author_version.appendChild(author_version_version_major);
|
||||
|
||||
QDomElement author_version_version_minor = doc.createElement("VersionMinor");
|
||||
text = doc.createTextNode("0");
|
||||
author_version_version_minor.appendChild(text);
|
||||
author_version.appendChild(author_version_version_minor);
|
||||
|
||||
QDomElement author_version_build_major = doc.createElement("BuildMajor");
|
||||
text = doc.createTextNode("0");
|
||||
author_version_build_major.appendChild(text);
|
||||
author_version.appendChild(author_version_build_major);
|
||||
|
||||
QDomElement author_version_build_minor = doc.createElement("BuildMinor");
|
||||
text = doc.createTextNode("0");
|
||||
author_version_build_minor.appendChild(text);
|
||||
author_version.appendChild(author_version_build_minor);
|
||||
|
||||
QDomElement author_type = doc.createElement("Type");
|
||||
text = doc.createTextNode("Beta"); // was GC_VERSION but should be Interna | Alpha | Beta | Release
|
||||
author_type.appendChild(text);
|
||||
author_build.appendChild(author_type);
|
||||
|
||||
QDomElement author_part_number = doc.createElement("PartNumber");
|
||||
text = doc.createTextNode("0");
|
||||
author_part_number.appendChild(text);
|
||||
author.appendChild(author_part_number);
|
||||
|
||||
|
||||
QByteArray xml = doc.toByteArray(4);
|
||||
if (!file.open(QIODevice::WriteOnly)) return(false);
|
||||
if (file.write(xml) != xml.size()) return(false);
|
||||
file.close();
|
||||
return(true);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
struct TcxFileReader : public RideFileReader {
|
||||
virtual RideFile *openRideFile(QFile &file, QStringList &errors) const;
|
||||
virtual bool writeRideFile(MainWindow *mainWindow, const QString cyclist, const RideFile *ride, QFile &file) const;
|
||||
};
|
||||
|
||||
#endif // _TcxRideFile_h
|
||||
|
||||
Reference in New Issue
Block a user