From f00ab725f333aba58e8f0961adfd5b7fe96337ab Mon Sep 17 00:00:00 2001 From: Damien Date: Fri, 5 Aug 2011 13:55:43 +0200 Subject: [PATCH] Add a TCX ride exporter --- src/MainWindow.cpp | 22 ++++ src/MainWindow.h | 1 + src/TcxRideFile.cpp | 277 ++++++++++++++++++++++++++++++++++++++++++++ src/TcxRideFile.h | 1 + 4 files changed, 301 insertions(+) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index bbab0f6da..f43331ccb 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -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() diff --git a/src/MainWindow.h b/src/MainWindow.h index 2ed3bdc70..8cbfa74b7 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -182,6 +182,7 @@ class MainWindow : public QMainWindow void downloadRide(); void manualRide(); void exportPWX(); + void exportTCX(); void exportCSV(); void exportGC(); void exportJson(); diff --git a/src/TcxRideFile.cpp b/src/TcxRideFile.cpp index 7b2509b14..3a538bbde 100644 --- a/src/TcxRideFile.cpp +++ b/src/TcxRideFile.cpp @@ -19,6 +19,14 @@ #include "TcxRideFile.h" #include "TcxParser.h" +#include + +#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 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); +} diff --git a/src/TcxRideFile.h b/src/TcxRideFile.h index 83324566f..ca48f083b 100644 --- a/src/TcxRideFile.h +++ b/src/TcxRideFile.h @@ -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