From ce10339c3c6243d38587f8b1281b2612ae085163 Mon Sep 17 00:00:00 2001 From: Paul Colby Date: Tue, 12 Aug 2014 13:05:44 +1000 Subject: [PATCH] Prevent TCX data truncation when using laps (#27) This is fairly extensive refactor of the TrainingSession::toTCX function to loop through samples instead of looping through laps, since the latter results in trailing samples being left out. --- src/polar/v2/trainingsession.cpp | 286 ++++++++------- src/polar/v2/trainingsession.h | 5 - .../testdata/training-sessions-22165267.tcx | 336 +++++++++++++++++- 3 files changed, 491 insertions(+), 136 deletions(-) diff --git a/src/polar/v2/trainingsession.cpp b/src/polar/v2/trainingsession.cpp index 4a59e609..47d91264 100644 --- a/src/polar/v2/trainingsession.cpp +++ b/src/polar/v2/trainingsession.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #ifdef Q_OS_WIN @@ -1551,6 +1552,40 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const const quint64 recordInterval = getDuration( firstMap(samples.value(QLatin1String("record-interval")))); + // Get the "samples" samples. + const QVariantList altitude = samples.value(QLatin1String("altitude")).toList(); + const QVariantList cadence = samples.value(QLatin1String("cadence")).toList(); + const QVariantList distance = samples.value(QLatin1String("distance")).toList(); + const QVariantList heartrate = samples.value(QLatin1String("heartrate")).toList(); + const QVariantList speed = samples.value(QLatin1String("speed")).toList(); + const QVariantList temperature = samples.value(QLatin1String("temperature")).toList(); + qDebug() << "samples sizes:" + << altitude.size() << cadence.size() << distance.size() + << heartrate.size() << speed.size() << temperature.size(); + + // Get the "route" samples. + const QVariantList duration = route.value(QLatin1String("duration")).toList(); + const QVariantList gpsAltitude = route.value(QLatin1String("altitude")).toList(); + const QVariantList latitude = route.value(QLatin1String("latitude")).toList(); + const QVariantList longitude = route.value(QLatin1String("longitude")).toList(); + const QVariantList satellites = route.value(QLatin1String("satellites")).toList(); + qDebug() << "route sizes:" << duration.size() << gpsAltitude.size() + << latitude.size() << longitude.size() << satellites.size(); + + const int maxIndex = + qMax(altitude.length(), + qMax(cadence.length(), + qMax(distance.length(), + qMax(heartrate.length(), + qMax(speed.length(), + //qMax(temperature.length(), // We don't use temperature in TCX yet. + qMax(duration.length(), + qMax(gpsAltitude.length(), + qMax(latitude.length(), + qMax(longitude.length(), + qMax(satellites.length(), 0)))))))))); + qDebug() << "max index" << maxIndex; + QDomElement activity = doc.createElement(QLatin1String("Activity")); if (multiSportSession.isNull()) { activities.appendChild(activity); @@ -1575,51 +1610,116 @@ QDomDocument TrainingSession::toTCX(const QString &buildTime) const activity.appendChild(doc.createElement(QLatin1String("Id"))) .appendChild(doc.createTextNode(startTime.toString(Qt::ISODate))); - // Determine which laps list to use. + // Build a map of lap split times to lap data. QVariantList laps = map.value(LAPS).toMap().value(QLatin1String("laps")).toList(); if (laps.isEmpty()) { laps = map.value(AUTOLAPS).toMap().value(QLatin1String("laps")).toList(); } + QMap splits; + foreach (const QVariant &lap, laps) { + const QVariantMap lapData = lap.toMap(); + const quint64 splitTime = getDuration(firstMap(firstMap( + lapData.value(QLatin1String("header"))) + .value(QLatin1String("split-time")))); + if (splitTime > 0) { + splits.insert(splitTime, lapData); + } + } // Add each of the laps to the Activity element. - int samplesIndex = 0; - for (int lapIndex = 0; lapIndex < qMax(1, laps.length()); ++lapIndex) { - // Prefect the lap-data, if any, so we only have to do it once. - const QVariantMap lapData = (laps.isEmpty()) - ? QVariantMap() : laps.at(lapIndex).toMap(); - - // Use either the exercise base (aka "create"), or the lap header. - const QVariantMap base = (laps.isEmpty()) ? create - : firstMap(lapData.value(QLatin1String("header"))); - - // Use either the excercise stats, or the lap stats. - const QVariantMap stats = (laps.isEmpty()) - ? map.value(STATISTICS).toMap() - : firstMap(lapData.value(QLatin1String("stats"))); - - // Create the Lap element, and set its StartTime attribute. - #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) - const QDateTime lapStartTime = startTime.addMSecs(samplesIndex * recordInterval); - #else /// @todo Remove this hack when Qt 5.2+ is available on Travis CI. - QDateTime lapStartTime = startTime.toUTC() - .addMSecs(samplesIndex * recordInterval).addSecs(startTime.utcOffset()); - lapStartTime.setUtcOffset(startTime.utcOffset()); - #endif - QDomElement lap = doc.createElement(QLatin1String("Lap")); - lap.setAttribute(QLatin1String("StartTime"), - lapStartTime.toString(Qt::ISODate)); - activity.appendChild(lap); - - // Add the per-lap (or per-exercise) statistics. - addLapStats(doc, lap, base, stats); - - // Add the lap's (or exercise's) sample data. - QDomElement track = doc.createElement(QLatin1String("Track")); - const quint64 splitTime = (laps.isEmpty()) ? 0 : - getDuration(firstMap(base.value(QLatin1String("split-time")))); - addTrackSamples(doc, track, samples, route, startTime, - splitTime, recordInterval, samplesIndex); - lap.appendChild(track); + QDomElement lap; + QVariantMap base = create; // The base data for this lap. + QVariantMap stats = map.value(STATISTICS).toMap(); + QDomElement track = doc.createElement(QLatin1String("Track")); + for (int index = 0; index < maxIndex; ++index) { + if ((lap.isNull()) || ((!splits.isEmpty()) && (index * recordInterval > splits.firstKey()))) { + if ((!lap.isNull()) && (!splits.isEmpty())) { + splits.remove(splits.firstKey()); + } + if (!splits.isEmpty()) { + const QVariantMap lapData = splits.first(); + base = firstMap(lapData.value(QLatin1String("header"))); + stats = firstMap(lapData.value(QLatin1String("stats"))); + } else if (index != 0) { + base = QVariantMap(); + stats = QVariantMap(); + } + + // Create the Lap element, and set its StartTime attribute. + #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) + const QDateTime lapStartTime = startTime.addMSecs(index * recordInterval); + #else /// @todo Remove this hack when Qt 5.2+ is available on Travis CI. + QDateTime lapStartTime = startTime.toUTC() + .addMSecs(index * recordInterval).addSecs(startTime.utcOffset()); + lapStartTime.setUtcOffset(startTime.utcOffset()); + #endif + lap = doc.createElement(QLatin1String("Lap")); + lap.setAttribute(QLatin1String("StartTime"), + lapStartTime.toString(Qt::ISODate)); + activity.appendChild(lap); + + // Add the per-lap (or per-exercise) statistics. + addLapStats(doc, lap, base, stats); + + track = doc.createElement(QLatin1String("Track")); + lap.appendChild(track); + } + + //const quint64 splitTime = (laps.isEmpty()) ? 0 : + //getDuration(firstMap(base.value(QLatin1String("split-time")))); + + + //addTrackSamples(doc, track, samples, route, startTime, + //splitTime, recordInterval, index); + + QDomElement trackPoint = doc.createElement(QLatin1String("Trackpoint")); + + if ((index < latitude.length()) && (index < longitude.length())) { + QDomElement position = doc.createElement(QLatin1String("Position")); + position.appendChild(doc.createElement(QLatin1String("LatitudeDegrees"))) + .appendChild(doc.createTextNode(latitude.at(index).toString())); + position.appendChild(doc.createElement(QLatin1String("LongitudeDegrees"))) + .appendChild(doc.createTextNode(longitude.at(index).toString())); + trackPoint.appendChild(position); + } + + if ((index < altitude.length()) && + (!sensorOffline(samples.value(QLatin1String("altitude-offline")).toList(), index))) { + trackPoint.appendChild(doc.createElement(QLatin1String("AltitudeMeters"))) + .appendChild(doc.createTextNode(altitude.at(index).toString())); + } + if ((index < distance.length()) && + (!sensorOffline(samples.value(QLatin1String("distance-offline")).toList(), index))) { + trackPoint.appendChild(doc.createElement(QLatin1String("DistanceMeters"))) + .appendChild(doc.createTextNode(distance.at(index).toString())); + } + if ((index < heartrate.length()) && (heartrate.at(index).toInt() > 0) && + (!sensorOffline(samples.value(QLatin1String("heartrate-offline")).toList(), index))) { + trackPoint.appendChild(doc.createElement(QLatin1String("HeartRateBpm"))) + .appendChild(doc.createElement(QLatin1String("Value"))) + .appendChild(doc.createTextNode(heartrate.at(index).toString())); + } + if ((index < cadence.length()) && (cadence.at(index).toInt() >= 0) && + (!sensorOffline(samples.value(QLatin1String("cadence-offline")).toList(), index))) { + trackPoint.appendChild(doc.createElement(QLatin1String("Cadence"))) + .appendChild(doc.createTextNode(cadence.at(index).toString())); + } + + if (trackPoint.hasChildNodes()) { + #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) + QDateTime trackPointTime = startTime.addMSecs(index * recordInterval); + #else /// @todo Remove this hack when Qt 5.2+ is available on Travis CI. + QDateTime trackPointTime = startTime.toUTC() + .addMSecs(index * recordInterval).addSecs(startTime.utcOffset()); + trackPointTime.setUtcOffset(startTime.utcOffset()); + #endif + trackPoint.insertBefore(doc.createElement(QLatin1String("Time")), QDomNode()) + .appendChild(doc.createTextNode(trackPointTime.toString(Qt::ISODate))); + track.appendChild(trackPoint); + } else { + /// @todo ? + //return; // We've exceeded the length of all data samples. + } } } @@ -1682,16 +1782,19 @@ void TrainingSession::addLapStats(QDomDocument &doc, QDomElement &lap, const QVariantMap &base, const QVariantMap &stats) const { + /// @todo Sort out trailing duration / distance. lap.appendChild(doc.createElement(QLatin1String("TotalTimeSeconds"))) .appendChild(doc.createTextNode(QString::fromLatin1("%1") .arg(getDuration(firstMap(base.value(QLatin1String("duration"))))/1000.0))); lap.appendChild(doc.createElement(QLatin1String("DistanceMeters"))) .appendChild(doc.createTextNode(QString::fromLatin1("%1") .arg(first(base.value(QLatin1String("distance"))).toDouble()))); - lap.appendChild(doc.createElement(QLatin1String("MaximumSpeed"))) - .appendChild(doc.createTextNode(QString::fromLatin1("%1") - .arg(first(firstMap(stats.value(QLatin1String("speed"))) - .value(QLatin1String("maximum"))).toDouble()))); + if (stats.contains(QLatin1String("speed"))) { + lap.appendChild(doc.createElement(QLatin1String("MaximumSpeed"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(firstMap(stats.value(QLatin1String("speed"))) + .value(QLatin1String("maximum"))).toDouble()))); + } // Calories is only available per exercise, not per lap, but it is required // by the TCX schema, so the following will set it to 0, if not present. @@ -1700,14 +1803,16 @@ void TrainingSession::addLapStats(QDomDocument &doc, QDomElement &lap, .arg(first(base.value(QLatin1String("calories"))).toUInt()))); const QVariantMap hrStats = firstMap(stats.value(QLatin1String("heartrate"))); - lap.appendChild(doc.createElement(QLatin1String("AverageHeartRateBpm"))) - .appendChild(doc.createElement(QLatin1String("Value"))) - .appendChild(doc.createTextNode(QString::fromLatin1("%1") - .arg(first(hrStats.value(QLatin1String("average"))).toUInt()))); - lap.appendChild(doc.createElement(QLatin1String("MaximumHeartRateBpm"))) - .appendChild(doc.createElement(QLatin1String("Value"))) - .appendChild(doc.createTextNode(QString::fromLatin1("%1") - .arg(first(hrStats.value(QLatin1String("maximum"))).toUInt()))); + if (!hrStats.isEmpty()) { + lap.appendChild(doc.createElement(QLatin1String("AverageHeartRateBpm"))) + .appendChild(doc.createElement(QLatin1String("Value"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(hrStats.value(QLatin1String("average"))).toUInt()))); + lap.appendChild(doc.createElement(QLatin1String("MaximumHeartRateBpm"))) + .appendChild(doc.createElement(QLatin1String("Value"))) + .appendChild(doc.createTextNode(QString::fromLatin1("%1") + .arg(first(hrStats.value(QLatin1String("maximum"))).toUInt()))); + } /// @todo Intensity must be one of: Active, Resting. lap.appendChild(doc.createElement(QLatin1String("Intensity"))) .appendChild(doc.createTextNode(QString::fromLatin1("Active"))); @@ -1732,85 +1837,6 @@ void TrainingSession::addLapStats(QDomDocument &doc, QDomElement &lap, .appendChild(doc.createTextNode(triggerMethod)); } -void TrainingSession::addTrackSamples(QDomDocument &doc, QDomElement &track, - const QVariantMap &samples, - const QVariantMap &route, - const QDateTime &startTime, - const quint64 splitTime, - const quint64 recordInterval, - int &index) const -{ - // Get the "samples" samples. - const QVariantList altitude = samples.value(QLatin1String("altitude")).toList(); - const QVariantList cadence = samples.value(QLatin1String("cadence")).toList(); - const QVariantList distance = samples.value(QLatin1String("distance")).toList(); - const QVariantList heartrate = samples.value(QLatin1String("heartrate")).toList(); - const QVariantList speed = samples.value(QLatin1String("speed")).toList(); - const QVariantList temperature = samples.value(QLatin1String("temperature")).toList(); - qDebug() << "samples sizes:" - << altitude.size() << cadence.size() << distance.size() - << heartrate.size() << speed.size() << temperature.size(); - - // Get the "route" samples. - const QVariantList duration = route.value(QLatin1String("duration")).toList(); - const QVariantList gpsAltitude = route.value(QLatin1String("altitude")).toList(); - const QVariantList latitude = route.value(QLatin1String("latitude")).toList(); - const QVariantList longitude = route.value(QLatin1String("longitude")).toList(); - const QVariantList satellites = route.value(QLatin1String("satellites")).toList(); - qDebug() << "route sizes:" << duration.size() << gpsAltitude.size() - << latitude.size() << longitude.size() << satellites.size(); - - for (; (splitTime == 0) || ((index * recordInterval) <= splitTime); ++index) { - QDomElement trackPoint = doc.createElement(QLatin1String("Trackpoint")); - - if ((index < latitude.length()) && (index < longitude.length())) { - QDomElement position = doc.createElement(QLatin1String("Position")); - position.appendChild(doc.createElement(QLatin1String("LatitudeDegrees"))) - .appendChild(doc.createTextNode(latitude.at(index).toString())); - position.appendChild(doc.createElement(QLatin1String("LongitudeDegrees"))) - .appendChild(doc.createTextNode(longitude.at(index).toString())); - trackPoint.appendChild(position); - } - - if ((index < altitude.length()) && - (!sensorOffline(samples.value(QLatin1String("altitude-offline")).toList(), index))) { - trackPoint.appendChild(doc.createElement(QLatin1String("AltitudeMeters"))) - .appendChild(doc.createTextNode(altitude.at(index).toString())); - } - if ((index < distance.length()) && - (!sensorOffline(samples.value(QLatin1String("distance-offline")).toList(), index))) { - trackPoint.appendChild(doc.createElement(QLatin1String("DistanceMeters"))) - .appendChild(doc.createTextNode(distance.at(index).toString())); - } - if ((index < heartrate.length()) && (heartrate.at(index).toInt() > 0) && - (!sensorOffline(samples.value(QLatin1String("heartrate-offline")).toList(), index))) { - trackPoint.appendChild(doc.createElement(QLatin1String("HeartRateBpm"))) - .appendChild(doc.createElement(QLatin1String("Value"))) - .appendChild(doc.createTextNode(heartrate.at(index).toString())); - } - if ((index < cadence.length()) && (cadence.at(index).toInt() >= 0) && - (!sensorOffline(samples.value(QLatin1String("cadence-offline")).toList(), index))) { - trackPoint.appendChild(doc.createElement(QLatin1String("Cadence"))) - .appendChild(doc.createTextNode(cadence.at(index).toString())); - } - - if (trackPoint.hasChildNodes()) { - #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) - QDateTime trackPointTime = startTime.addMSecs(index * recordInterval); - #else /// @todo Remove this hack when Qt 5.2+ is available on Travis CI. - QDateTime trackPointTime = startTime.toUTC() - .addMSecs(index * recordInterval).addSecs(startTime.utcOffset()); - trackPointTime.setUtcOffset(startTime.utcOffset()); - #endif - trackPoint.insertBefore(doc.createElement(QLatin1String("Time")), QDomNode()) - .appendChild(doc.createTextNode(trackPointTime.toString(Qt::ISODate))); - track.appendChild(trackPoint); - } else { - return; // We've exceeded the length of all data samples. - } - } -} - QByteArray TrainingSession::unzip(const QByteArray &data, const int initialBufferSize) const { diff --git a/src/polar/v2/trainingsession.h b/src/polar/v2/trainingsession.h index 9b673755..841c4929 100644 --- a/src/polar/v2/trainingsession.h +++ b/src/polar/v2/trainingsession.h @@ -102,11 +102,6 @@ class TrainingSession : public QObject { void addLapStats(QDomDocument &doc, QDomElement &lap, const QVariantMap &base, const QVariantMap &stats) const; - void addTrackSamples(QDomDocument &doc, QDomElement &track, - const QVariantMap &samples, const QVariantMap &route, - const QDateTime &startTime, const quint64 splitTime, - const quint64 recordInterval, int &index) const; - }; }} diff --git a/test/polar/v2/testdata/training-sessions-22165267.tcx b/test/polar/v2/testdata/training-sessions-22165267.tcx index 9ce0cff6..0b9e3bd2 100644 --- a/test/polar/v2/testdata/training-sessions-22165267.tcx +++ b/test/polar/v2/testdata/training-sessions-22165267.tcx @@ -1,5 +1,5 @@ - + 2014-08-07T17:25:01+10:00 @@ -27533,6 +27533,340 @@ + + 0 + 0 + 0 + Active + Manual + + + + + -38.0441383333333 + 145.331948333333 + + -100.585 + 5999.7 + + 168 + + 81 + + + + + -38.04414 + 145.331915 + + -100.585 + 6001.8 + + 168 + + 81 + + + + + -38.0441366666667 + 145.331881666667 + + -100.585 + 6005.8 + + 168 + + 82 + + + + + -38.044135 + 145.33185 + + -101.652 + 6008 + + 168 + + 82 + + + + + -38.0441333333333 + 145.331816666667 + + -101.652 + 6010.1 + + 168 + + 82 + + + + + -38.0441283333333 + 145.331783333333 + + -101.652 + 6014.3 + + 167 + + 81 + + + + + -38.0441233333333 + 145.33175 + + -101.652 + 6016.2 + + 167 + + 81 + + + + + -38.044115 + 145.331715 + + -101.652 + 6018.3 + + 167 + + 81 + + + + + -38.0441083333333 + 145.331681666667 + + -101.652 + 6022.4 + + 167 + + 81 + + + + + -38.044105 + 145.33165 + + -101.652 + 6024.6 + + 167 + + 80 + + + + + -38.0441016666667 + 145.331618333333 + + -101.652 + 6026.7 + + 167 + + 81 + + + + + -38.0441 + 145.331585 + + -101.652 + 6031.2 + + 167 + + 81 + + + + + -38.0440966666667 + 145.331553333333 + + -101.652 + 6033.3 + + 167 + + 81 + + + + + -38.0440883333333 + 145.331521666667 + + -101.652 + 6035.4 + + 167 + + 81 + + + + + -38.044075 + 145.331496666667 + + -101.652 + 6039.7 + + 167 + + 79 + + + + + -38.0440633333333 + 145.331478333333 + + -101.652 + 6039.7 + + 167 + + 79 + + + + + -38.04405 + 145.331471666667 + + -101.652 + 6041.7 + + 167 + + 76 + + + + + -38.0440483333333 + 145.331471666667 + + -101.652 + 6043.8 + + 167 + + 70 + + + + + -38.0440433333333 + 145.331471666667 + + -101.652 + 6043.8 + + 167 + + 59 + + + + + -38.0440416666667 + 145.331471666667 + + -101.652 + 6043.8 + + 167 + + 59 + + + + + -38.0440416666667 + 145.331473333333 + + -101.652 + 6043.8 + + 166 + + 0 + + + + + -38.04404 + 145.331473333333 + + -101.652 + 6043.8 + + 166 + + 0 + + + + + -38.04404 + 145.331475 + + -101.652 + 6043.8 + + 166 + + 0 + + + + + -38.0440383333333 + 145.331475 + + -101.652 + 6043.8 + + 165 + + 0 + + + + + -38.0440383333333 + 145.331476666667 + + -101.652 + 6043.8 + + 165 + + 0 + + +