merge heads

This commit is contained in:
Jimmy Shih
2012-02-29 15:20:18 -08:00
2 changed files with 251 additions and 257 deletions
@@ -23,85 +23,90 @@ import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.lib.R; import com.google.android.apps.mytracks.lib.R;
import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.SystemUtils;
import android.content.Context; import android.content.Context;
import android.location.Location; import android.location.Location;
import android.os.Build;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
/** /**
* Write out a a track in the Garmin training center database, tcx format. * Write track as TCX to a file. See http://developer.garmin.com/schemas/tcx/v2/
* As defined by: * for info on TCX. <br>
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 * The TCX file output is verified by uploading the file to
* * http://connect.garmin.com/.
* The TCX file written by this class has been verified as compatible with
* Garmin Training Center 3.5.3.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Dominik Ršttsches * @author Dominik Ršttsches
*/ */
public class TcxTrackWriter implements TrackFormatWriter { public class TcxTrackWriter implements TrackFormatWriter {
// "Biking" related string IDs /**
* TCX sport type. See the TCX spec.
*
* @author Jimmy Shih
*/
private enum SportType {
RUNNING("Running"),
BIKING("Biking"),
OTHER("Other");
private final String name;
private SportType(String name) {
this.name = name;
}
/**
* Gets the name of the sport type
*/
public String getName() {
return name;
}
}
// My Tracks categories that are considered as TCX biking sport type.
private static final int TCX_SPORT_BIKING_IDS[] = { private static final int TCX_SPORT_BIKING_IDS[] = {
R.string.activity_type_cycling, R.string.activity_type_cycling,
R.string.activity_type_dirt_bike, R.string.activity_type_dirt_bike,
R.string.activity_type_mountain_biking, R.string.activity_type_mountain_biking,
R.string.activity_type_road_biking, R.string.activity_type_road_biking,
R.string.activity_type_track_cycling, R.string.activity_type_track_cycling };
};
// "Running" related string IDs // My Tracks categories that are considered as TCX running sport type.
private static final int TCX_SPORT_RUNNING_IDS[] = { private static final int TCX_SPORT_RUNNING_IDS[] = {
R.string.activity_type_running, R.string.activity_type_running,
R.string.activity_type_speed_walking, R.string.activity_type_speed_walking,
R.string.activity_type_street_running, R.string.activity_type_street_running,
R.string.activity_type_track_running, R.string.activity_type_track_running,
R.string.activity_type_trail_running, R.string.activity_type_trail_running,
R.string.activity_type_walking, R.string.activity_type_walking };
};
// These are the only sports allowed by the TCX v2 specification for fields
// of type Sport_t.
private static final String TCX_SPORT_BIKING = "Biking";
private static final String TCX_SPORT_RUNNING = "Running";
private static final String TCX_SPORT_OTHER = "Other";
// Values for fields of type Build_t/Type.
private static final String TCX_TYPE_RELEASE = "Release";
private static final String TCX_TYPE_INTERNAL = "Internal";
private final Context context; private final Context context;
private PrintWriter pw = null;
private Track track; private Track track;
private PrintWriter printWriter;
// Determines whether to encode cadence value as running or cycling cadence. private SportType sportType;
private boolean sportIsCycling;
public TcxTrackWriter(Context context) { public TcxTrackWriter(Context context) {
this.context = context; this.context = context;
} }
@SuppressWarnings("hiding")
@Override @Override
public void prepare(Track track, OutputStream out) { public void prepare(Track aTrack, OutputStream out) {
this.track = track; this.track = aTrack;
this.pw = new PrintWriter(out); this.printWriter = new PrintWriter(out);
this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING); this.sportType = getSportType(track.getCategory());
} }
@Override @Override
public void close() { public void close() {
if (pw != null) { if (printWriter != null) {
pw.close(); printWriter.close();
pw = null; printWriter = null;
} }
} }
@@ -112,246 +117,216 @@ public class TcxTrackWriter implements TrackFormatWriter {
@Override @Override
public void writeHeader() { public void writeHeader() {
if (pw == null) { if (printWriter != null) {
return; printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<TrainingCenterDatabase"
+ " xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation="
+ "\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"
+ " http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("<Author xsi:type=\"Application_t\">");
printWriter.println("<Name>"
+ StringUtils.stringAsCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <Build>, <LangID>, and <PartNumber> are required by type=Application_t.
printWriter.println("<Build>");
writeVersion();
printWriter.println("</Build>");
printWriter.println("<LangID>" + Locale.getDefault().getLanguage() + "</LangID>");
printWriter.println("<PartNumber>000-00000-00</PartNumber>");
printWriter.println("</Author>");
printWriter.println("</TrainingCenterDatabase>");
} }
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"no\" ?>\n",
Charset.defaultCharset().name());
pw.print("<TrainingCenterDatabase ");
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
pw.print("xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 ");
pw.println("http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
pw.println();
} }
@Override @Override
public void writeBeginTrack(Location firstPoint) { public void writeBeginTrack(Location firstPoint) {
if (pw == null) { if (printWriter != null) {
return; String startTime = FileUtils.FILE_TIMESTAMP_FORMAT.format(
} track.getStatistics().getStartTime());
long totalTimeInSeconds = track.getStatistics().getTotalTime() / 1000;
String startTime = FileUtils.FILE_TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime()); printWriter.println("<Activities>");
printWriter.println("<Activity Sport=\"" + sportType.getName() + "\">");
pw.println(" <Activities>"); printWriter.println("<Id>" + startTime + "</Id>");
pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory())); printWriter.println("<Lap StartTime=\"" + startTime + "\">");
pw.format(" <Id>%s</Id>\n", startTime); printWriter.println("<TotalTimeSeconds>" + totalTimeInSeconds + "</TotalTimeSeconds>");
pw.format(" <Lap StartTime=\"%s\">\n", startTime); printWriter.println("<DistanceMeters>" + track.getStatistics().getTotalDistance()
pw.print(" <TotalTimeSeconds>"); + "</DistanceMeters>");
pw.print(track.getStatistics().getTotalTime() / 1000); // <Calories> is required, just put in 0.
pw.println("</TotalTimeSeconds>"); printWriter.println("<Calories>0</Calories>");
pw.print(" <DistanceMeters>"); printWriter.println("<Intensity>Active</Intensity>");
pw.print(track.getStatistics().getTotalDistance()); printWriter.println("<TriggerMethod>Manual</TriggerMethod>");
pw.println("</DistanceMeters>");
// TODO max speed etc.
// Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>");
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.println(" <Track>");
}
}
@Override
public void writeLocation(Location location) {
if (pw == null) {
return;
}
pw.println(" <Trackpoint>");
Date d = new Date(location.getTime());
pw.println(" <Time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(d) + "</Time>");
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
pw.print(location.getLatitude());
pw.println("</LatitudeDegrees>");
pw.print(" <LongitudeDegrees>");
pw.print(location.getLongitude());
pw.println("</LongitudeDegrees>");
pw.println(" </Position>");
pw.print(" <AltitudeMeters>");
pw.print(location.getAltitude());
pw.println("</AltitudeMeters>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
if (sensorData != null) {
if (sensorData.hasHeartRate()
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorData.getHeartRate().hasValue()) {
pw.print(" <HeartRateBpm>");
pw.print("<Value>");
pw.print(sensorData.getHeartRate().getValue());
pw.print("</Value>");
pw.println("</HeartRateBpm>");
}
boolean cadenceAvailable = sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue();
// TCX Trackpoint_t contains a sequence. Thus, the legacy XML element
// <Cadence> needs to be put before <Extensions>.
// This field should only be used for the case that activity was marked as biking.
// Otherwise cadence is interpreted as running cadence data which
// is written in the <Extensions> as <RunCadence>.
if (sportIsCycling && cadenceAvailable) {
pw.print(" <Cadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.println("</Cadence>");
}
boolean powerAvailable = sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue();
if(powerAvailable || (!sportIsCycling && cadenceAvailable)) {
pw.print(" <Extensions>");
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// RunCadence needs to be put before power in order to be understood
// by Garmin Training Center.
if (!sportIsCycling && cadenceAvailable) {
pw.print("<RunCadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.print("</RunCadence>");
}
if (powerAvailable) {
pw.print("<Watts>");
pw.print(sensorData.getPower().getValue());
pw.print("</Watts>");
}
pw.println("</TPX></Extensions>");
}
}
}
pw.println(" </Trackpoint>");
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println(" </Track>");
} }
} }
@Override @Override
public void writeEndTrack(Location lastPoint) { public void writeEndTrack(Location lastPoint) {
if (pw == null) { if (printWriter != null) {
return; printWriter.println("</Lap>");
printWriter.println("<Creator xsi:type=\"Device_t\">");
printWriter.println("<Name>"
+ StringUtils.stringAsCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <UnitId>, <ProductID>, and <Version> are required for type=Device_t.
printWriter.println("<UnitId>0</UnitId>");
printWriter.println("<ProductID>0</ProductID>");
writeVersion();
printWriter.println("</Creator>");
printWriter.println("</Activity>");
printWriter.println("</Activities>");
} }
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>My Tracks running on %s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
writeVersion();
pw.println("</Creator>");
pw.println(" </Activity>");
pw.println(" </Activities>");
} }
@Override @Override
public void writeFooter() { public void writeOpenSegment() {
if (pw == null) { if (printWriter != null) {
return; printWriter.println("<Track>");
} }
pw.println(" <Author xsi:type=\"Application_t\">"); }
// We put the version in the name because there isn't a better place for @Override
// it. The TCX schema tightly defined the Version tag, so we can't put it public void writeCloseSegment() {
// there. They've similarly constrained the PartNumber tag, so it can't go if (printWriter != null) {
// there either. printWriter.println("</Track>");
pw.format("<Name>My Tracks %s by Google</Name>\n", SystemUtils.getMyTracksVersion(context)); }
}
pw.println("<Build>"); @Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<Trackpoint>");
printWriter.println("<Time>"
+ FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime())) + "</Time>");
printWriter.println("<Position>");
printWriter.println("<LatitudeDegrees>" + location.getLatitude() + "</LatitudeDegrees>");
printWriter.println("<LongitudeDegrees>" + location.getLongitude() + "</LongitudeDegrees>");
printWriter.println("</Position>");
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
writeVersion(); if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
boolean heartRateAvailable = sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING;
boolean cadenceAvailable = sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING;
boolean powerAvailable = sensorDataSet.hasPower()
&& sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING;
if (heartRateAvailable) {
printWriter.println("<HeartRateBpm>");
printWriter.println("<Value>" + sensorDataSet.getHeartRate().getValue() + "</Value>");
printWriter.println("</HeartRateBpm>");
}
pw.format("<Type>%s</Type>\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE // <Cadence> needs to be put before <Extensions>.
: TCX_TYPE_INTERNAL); // According to the TCX spec, <Cadence> is only for the biking sport
pw.println("</Build>"); // type. For others, use <RunCadence> in <Extensions>.
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage()); if (cadenceAvailable && sportType == SportType.BIKING) {
pw.println("<PartNumber>000-00000-00</PartNumber>"); // The spec requires the max value be 254.
pw.println("</Author>"); printWriter.println(
pw.println("</TrainingCenterDatabase>"); "<Cadence>" + Math.min(254, sensorDataSet.getCadence().getValue()) + "</Cadence>");
}
if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
printWriter.println("<Extensions>");
printWriter.println(
"<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// <RunCadence> needs to be put before <Watts>.
if (cadenceAvailable && sportType != SportType.BIKING) {
// The spec requires the max value to be 254.
printWriter.println("<RunCadence>"
+ Math.min(254, sensorDataSet.getCadence().getValue()) + "</RunCadence>");
}
if (powerAvailable) {
printWriter.println("<Watts>" + sensorDataSet.getPower().getValue() + "</Watts>");
}
printWriter.println("</TPX>");
printWriter.println("</Extensions>");
}
}
}
printWriter.println("</Trackpoint>");
}
} }
@Override @Override
public void writeBeginWaypoints() { public void writeBeginWaypoints() {
// Do nothing // Do nothing.
} }
@Override @Override
public void writeEndWaypoints() { public void writeEndWaypoints() {
// Do nothing // Do nothing.
} }
@Override @Override
public void writeWaypoint(Waypoint waypoint) { public void writeWaypoint(Waypoint waypoint) {
// TODO Write out the waypoints somewhere. // Do nothing.
} }
/**
* Writes the TCX Version.
*/
private void writeVersion() { private void writeVersion() {
if (pw == null) { // Split the My Tracks version code into VersionMajor, VersionMinor, and,
return; // BuildMajor to fit the integer type requirement for these fields in the
} // TCX spec.
String[] versionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
int versionMajor = versionComponents.length > 0 ? Integer.valueOf(versionComponents[0]) : 0;
int versionMinor = versionComponents.length > 1 ? Integer.valueOf(versionComponents[1]) : 0;
int buildMajor = versionComponents.length > 2 ? Integer.valueOf(versionComponents[2]) : 0;
// Splitting the myTracks version code into VersionMajor, VersionMinor and BuildMajor printWriter.println("<Version>");
// to fit the integer type requirement for these fields in the TCX spec. printWriter.println("<VersionMajor>" + versionMajor + "</VersionMajor>");
// Putting a string like "x.x.x" into VersionMajor breaks XML validation. printWriter.println("<VersionMinor>" + versionMinor + "</VersionMinor>");
// We also set the BuildMinor version to 1 if this is a development build to // According to TCX spec, these are optional. But http://connect.garmin.com
// signify that this build is newer than the one associated with the // requires them.
// version code given in BuildMajor. printWriter.println("<BuildMajor>" + buildMajor + "</BuildMajor>");
printWriter.println("<BuildMinor>0</BuildMinor>");
String[] myTracksVersionComponents = SystemUtils.getMyTracksVersion(context).split("\\."); printWriter.println("</Version>");
pw.println("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", Integer.valueOf(myTracksVersionComponents[0]));
pw.format("<VersionMinor>%d</VersionMinor>\n", Integer.valueOf(myTracksVersionComponents[1]));
// TCX schema says these are optional but http://connect.garmin.com only accepts
// the TCX file when they are present.
pw.format("<BuildMajor>%d</BuildMajor>\n", Integer.valueOf(myTracksVersionComponents[2]));
pw.format("<BuildMinor>%d</BuildMinor>\n", SystemUtils.isRelease(context) ? 0 : 1);
pw.println("</Version>");
} }
private String categoryToTcxSport(String category) { /**
* Gets the sport type from the category.
*
* @param category the category
*/
private SportType getSportType(String category) {
category = category.trim(); category = category.trim();
// category is possibly localized here, so compare it to localized string resources // For tracks with localized category.
for (int i : TCX_SPORT_RUNNING_IDS) { for (int i : TCX_SPORT_RUNNING_IDS) {
if (category.equalsIgnoreCase(context.getResources().getString(i))) { if (category.equalsIgnoreCase(context.getString(i))) {
return TCX_SPORT_RUNNING; return SportType.RUNNING;
} }
} }
for (int i : TCX_SPORT_BIKING_IDS) { for (int i : TCX_SPORT_BIKING_IDS) {
if (category.equalsIgnoreCase(context.getResources().getString(i))) { if (category.equalsIgnoreCase(context.getString(i))) {
return TCX_SPORT_BIKING; return SportType.BIKING;
} }
} }
// for tracks without localized activity type // For tracks without localized category.
if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) { if (category.equalsIgnoreCase(SportType.RUNNING.getName())) {
return TCX_SPORT_RUNNING; return SportType.RUNNING;
} else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) { } else if (category.equalsIgnoreCase(SportType.BIKING.getName())) {
return TCX_SPORT_BIKING; return SportType.BIKING;
} else { } else {
return TCX_SPORT_OTHER; return SportType.OTHER;
} }
} }
} }
@@ -1,4 +1,18 @@
// Copyright 2010 Google Inc. All Rights Reserved. /*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file; package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksLocation;
@@ -11,7 +25,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
/** /**
* Tests for the GPX track exporter. * Tests for {@link TcxTrackWriter}.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -37,42 +51,47 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest {
} }
/** /**
* Asserts that the given tags describe the given points, in the same order. * Asserts that the given tags describe the given locations in the same order.
*
* @param tags list of tags
* @param locations list of locations
*/ */
private void assertTagsMatchPoints(List<Element> tags, MyTracksLocation... locs) { private void assertTagsMatchPoints(List<Element> tags, MyTracksLocation... locations) {
assertEquals(locs.length, tags.size()); assertEquals(locations.length, tags.size());
for (int i = 0; i < locs.length; i++) { for (int i = 0; i < locations.length; i++) {
Element tag = tags.get(i); assertTagMatchesLocation(tags.get(i), locations[i]);
MyTracksLocation loc = locs[i];
assertTagMatchesLocation(tag, loc);
} }
} }
/** /**
* Asserts that the given tag describes the given location. * Asserts that the given tag describes the given location.
*
* @param tag the tag
* @param location the location
*/ */
private void assertTagMatchesLocation(Element tag, MyTracksLocation loc) { private void assertTagMatchesLocation(Element tag, MyTracksLocation location) {
Element posTag = getChildElement(tag, "Position"); assertEquals(
assertEquals(Double.toString(loc.getLatitude()), FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()), getChildTextValue(tag, "Time"));
getChildTextValue(posTag, "LatitudeDegrees"));
assertEquals(Double.toString(loc.getLongitude()),
getChildTextValue(posTag, "LongitudeDegrees"));
assertEquals(FileUtils.FILE_TIMESTAMP_FORMAT.format(loc.getTime()), Element positionTag = getChildElement(tag, "Position");
getChildTextValue(tag, "Time")); assertEquals(
assertEquals(Double.toString(loc.getAltitude()), Double.toString(location.getLatitude()), getChildTextValue(positionTag, "LatitudeDegrees"));
getChildTextValue(tag, "AltitudeMeters")); assertEquals(Double.toString(location.getLongitude()),
assertTrue(loc.getSensorDataSet() != null); getChildTextValue(positionTag, "LongitudeDegrees"));
Sensor.SensorDataSet sds = loc.getSensorDataSet();
List<Element> bpm = getChildElements(tag, "HeartRateBpm", 1); assertEquals(Double.toString(location.getAltitude()), getChildTextValue(tag, "AltitudeMeters"));
assertTrue(location.getSensorDataSet() != null);
Sensor.SensorDataSet sds = location.getSensorDataSet();
List<Element> heartRate = getChildElements(tag, "HeartRateBpm", 1);
assertEquals(Integer.toString(sds.getHeartRate().getValue()), assertEquals(Integer.toString(sds.getHeartRate().getValue()),
getChildTextValue(bpm.get(0), "Value")); getChildTextValue(heartRate.get(0), "Value"));
List<Element> ext = getChildElements(tag, "Extensions", 1); List<Element> extensions = getChildElements(tag, "Extensions", 1);
List<Element> tpx = getChildElements(ext.get(0), "TPX", 1); List<Element> tpx = getChildElements(extensions.get(0), "TPX", 1);
assertEquals(Integer.toString(sds.getPower().getValue()), assertEquals(
getChildTextValue(tpx.get(0), "Watts")); Integer.toString(sds.getCadence().getValue()), getChildTextValue(tpx.get(0), "RunCadence"));
assertEquals(
Integer.toString(sds.getPower().getValue()), getChildTextValue(tpx.get(0), "Watts"));
} }
} }