diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java
index 7085a8f79..2613aab80 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java
@@ -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.lib.R;
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 android.content.Context;
import android.location.Location;
-import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
-import java.nio.charset.Charset;
import java.util.Date;
import java.util.Locale;
/**
- * Write out a a track in the Garmin training center database, tcx format.
- * As defined by:
- * http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
- *
- * The TCX file written by this class has been verified as compatible with
- * Garmin Training Center 3.5.3.
+ * Write track as TCX to a file. See http://developer.garmin.com/schemas/tcx/v2/
+ * for info on TCX.
+ * The TCX file output is verified by uploading the file to
+ * http://connect.garmin.com/.
*
* @author Sandor Dornbush
* @author Dominik RĀttsches
*/
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[] = {
R.string.activity_type_cycling,
R.string.activity_type_dirt_bike,
R.string.activity_type_mountain_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[] = {
R.string.activity_type_running,
R.string.activity_type_speed_walking,
R.string.activity_type_street_running,
R.string.activity_type_track_running,
R.string.activity_type_trail_running,
- 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";
+ R.string.activity_type_walking };
private final Context context;
-
- private PrintWriter pw = null;
private Track track;
-
- // Determines whether to encode cadence value as running or cycling cadence.
- private boolean sportIsCycling;
+ private PrintWriter printWriter;
+ private SportType sportType;
public TcxTrackWriter(Context context) {
this.context = context;
}
- @SuppressWarnings("hiding")
@Override
- public void prepare(Track track, OutputStream out) {
- this.track = track;
- this.pw = new PrintWriter(out);
- this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING);
+ public void prepare(Track aTrack, OutputStream out) {
+ this.track = aTrack;
+ this.printWriter = new PrintWriter(out);
+ this.sportType = getSportType(track.getCategory());
}
@Override
public void close() {
- if (pw != null) {
- pw.close();
- pw = null;
+ if (printWriter != null) {
+ printWriter.close();
+ printWriter = null;
}
}
@@ -112,246 +117,216 @@ public class TcxTrackWriter implements TrackFormatWriter {
@Override
public void writeHeader() {
- if (pw == null) {
- return;
+ if (printWriter != null) {
+ printWriter.println("");
+ printWriter.println("");
+ }
+ }
+
+ @Override
+ public void writeFooter() {
+ if (printWriter != null) {
+ printWriter.println("");
+ printWriter.println(""
+ + StringUtils.stringAsCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ + "");
+ // , , and are required by type=Application_t.
+ printWriter.println("");
+ writeVersion();
+ printWriter.println("");
+ printWriter.println("" + Locale.getDefault().getLanguage() + "");
+ printWriter.println("000-00000-00");
+ printWriter.println("");
+ printWriter.println("");
}
- pw.format("\n",
- Charset.defaultCharset().name());
- pw.print("");
- pw.println();
}
@Override
public void writeBeginTrack(Location firstPoint) {
- if (pw == null) {
- return;
- }
+ if (printWriter != null) {
+ 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());
-
- pw.println(" ");
- pw.format(" \n", categoryToTcxSport(track.getCategory()));
- pw.format(" %s\n", startTime);
- pw.format(" \n", startTime);
- pw.print(" ");
- pw.print(track.getStatistics().getTotalTime() / 1000);
- pw.println("");
- pw.print(" ");
- pw.print(track.getStatistics().getTotalDistance());
- pw.println("");
- // TODO max speed etc.
- // Calories are a required element just put in 0.
- pw.print("0");
- pw.println("Active");
- pw.println("Manual");
- }
-
- @Override
- public void writeOpenSegment() {
- if (pw != null) {
- pw.println(" ");
+ printWriter.println("");
+ printWriter.println("");
+ printWriter.println("" + startTime + "");
+ printWriter.println("");
+ printWriter.println("" + totalTimeInSeconds + "");
+ printWriter.println("" + track.getStatistics().getTotalDistance()
+ + "");
+ // is required, just put in 0.
+ printWriter.println("0");
+ printWriter.println("Active");
+ printWriter.println("Manual");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
- if (pw == null) {
- return;
+ if (printWriter != null) {
+ printWriter.println("");
+ printWriter.println("");
+ printWriter.println(""
+ + StringUtils.stringAsCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ + "");
+ // , , and are required for type=Device_t.
+ printWriter.println("0");
+ printWriter.println("0");
+ writeVersion();
+ printWriter.println("");
+ printWriter.println("");
+ printWriter.println("");
}
- pw.println(" ");
- pw.print(" ");
- pw.format("My Tracks running on %s\n", Build.MODEL);
-
- // The following code is correct. ID is inconsistently capitalized in the
- // TCX schema.
- pw.println("0");
- pw.println("0");
-
- writeVersion();
-
- pw.println("");
- pw.println(" ");
- pw.println(" ");
}
@Override
- public void writeFooter() {
- if (pw == null) {
- return;
+ public void writeOpenSegment() {
+ if (printWriter != null) {
+ printWriter.println("");
+ }
+ }
- pw.println("");
+ @Override
+ public void writeLocation(Location location) {
+ if (printWriter != null) {
+ printWriter.println("");
+ printWriter.println("");
+ printWriter.println("");
+ printWriter.println("" + location.getLatitude() + "");
+ printWriter.println("" + location.getLongitude() + "");
+ printWriter.println("");
+ printWriter.println("" + location.getAltitude() + "");
- 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("");
+ printWriter.println("" + sensorDataSet.getHeartRate().getValue() + "");
+ printWriter.println("");
+ }
- pw.format("%s\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE
- : TCX_TYPE_INTERNAL);
- pw.println("");
- pw.format("%s\n", Locale.getDefault().getLanguage());
- pw.println("000-00000-00");
- pw.println("");
- pw.println("");
+ // needs to be put before .
+ // According to the TCX spec, is only for the biking sport
+ // type. For others, use in .
+ if (cadenceAvailable && sportType == SportType.BIKING) {
+ // The spec requires the max value be 254.
+ printWriter.println(
+ "" + Math.min(254, sensorDataSet.getCadence().getValue()) + "");
+ }
+
+ if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
+ printWriter.println("");
+ printWriter.println(
+ "");
+
+ // needs to be put before .
+ if (cadenceAvailable && sportType != SportType.BIKING) {
+ // The spec requires the max value to be 254.
+ printWriter.println(""
+ + Math.min(254, sensorDataSet.getCadence().getValue()) + "");
+ }
+
+ if (powerAvailable) {
+ printWriter.println("" + sensorDataSet.getPower().getValue() + "");
+ }
+ printWriter.println("");
+ printWriter.println("");
+ }
+ }
+ }
+ printWriter.println("");
+ }
}
@Override
public void writeBeginWaypoints() {
- // Do nothing
+ // Do nothing.
}
@Override
public void writeEndWaypoints() {
- // Do nothing
+ // Do nothing.
}
@Override
public void writeWaypoint(Waypoint waypoint) {
- // TODO Write out the waypoints somewhere.
+ // Do nothing.
}
+ /**
+ * Writes the TCX Version.
+ */
private void writeVersion() {
- if (pw == null) {
- return;
- }
+ // Split the My Tracks version code into VersionMajor, VersionMinor, and,
+ // 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
- // to fit the integer type requirement for these fields in the TCX spec.
- // Putting a string like "x.x.x" into VersionMajor breaks XML validation.
- // We also set the BuildMinor version to 1 if this is a development build to
- // signify that this build is newer than the one associated with the
- // version code given in BuildMajor.
-
- String[] myTracksVersionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
-
- pw.println("");
- pw.format("%d\n", Integer.valueOf(myTracksVersionComponents[0]));
- pw.format("%d\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("%d\n", Integer.valueOf(myTracksVersionComponents[2]));
- pw.format("%d\n", SystemUtils.isRelease(context) ? 0 : 1);
- pw.println("");
+ printWriter.println("");
+ printWriter.println("" + versionMajor + "");
+ printWriter.println("" + versionMinor + "");
+ // According to TCX spec, these are optional. But http://connect.garmin.com
+ // requires them.
+ printWriter.println("" + buildMajor + "");
+ printWriter.println("0");
+ printWriter.println("");
}
- 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 is possibly localized here, so compare it to localized string resources
+ // For tracks with localized category.
for (int i : TCX_SPORT_RUNNING_IDS) {
- if (category.equalsIgnoreCase(context.getResources().getString(i))) {
- return TCX_SPORT_RUNNING;
+ if (category.equalsIgnoreCase(context.getString(i))) {
+ return SportType.RUNNING;
}
}
for (int i : TCX_SPORT_BIKING_IDS) {
- if (category.equalsIgnoreCase(context.getResources().getString(i))) {
- return TCX_SPORT_BIKING;
+ if (category.equalsIgnoreCase(context.getString(i))) {
+ return SportType.BIKING;
}
}
- // for tracks without localized activity type
- if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
- return TCX_SPORT_RUNNING;
- } else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
- return TCX_SPORT_BIKING;
+ // For tracks without localized category.
+ if (category.equalsIgnoreCase(SportType.RUNNING.getName())) {
+ return SportType.RUNNING;
+ } else if (category.equalsIgnoreCase(SportType.BIKING.getName())) {
+ return SportType.BIKING;
} else {
- return TCX_SPORT_OTHER;
+ return SportType.OTHER;
}
}
}
diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java
index 6594cf51c..a120b4999 100644
--- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java
+++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java
@@ -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;
import com.google.android.apps.mytracks.content.MyTracksLocation;
@@ -11,7 +25,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
- * Tests for the GPX track exporter.
+ * Tests for {@link TcxTrackWriter}.
*
* @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 tags, MyTracksLocation... locs) {
- assertEquals(locs.length, tags.size());
- for (int i = 0; i < locs.length; i++) {
- Element tag = tags.get(i);
- MyTracksLocation loc = locs[i];
-
- assertTagMatchesLocation(tag, loc);
+ private void assertTagsMatchPoints(List tags, MyTracksLocation... locations) {
+ assertEquals(locations.length, tags.size());
+ for (int i = 0; i < locations.length; i++) {
+ assertTagMatchesLocation(tags.get(i), locations[i]);
}
}
/**
* Asserts that the given tag describes the given location.
+ *
+ * @param tag the tag
+ * @param location the location
*/
- private void assertTagMatchesLocation(Element tag, MyTracksLocation loc) {
- Element posTag = getChildElement(tag, "Position");
- assertEquals(Double.toString(loc.getLatitude()),
- getChildTextValue(posTag, "LatitudeDegrees"));
- assertEquals(Double.toString(loc.getLongitude()),
- getChildTextValue(posTag, "LongitudeDegrees"));
+ private void assertTagMatchesLocation(Element tag, MyTracksLocation location) {
+ assertEquals(
+ FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()), getChildTextValue(tag, "Time"));
- assertEquals(FileUtils.FILE_TIMESTAMP_FORMAT.format(loc.getTime()),
- getChildTextValue(tag, "Time"));
- assertEquals(Double.toString(loc.getAltitude()),
- getChildTextValue(tag, "AltitudeMeters"));
- assertTrue(loc.getSensorDataSet() != null);
- Sensor.SensorDataSet sds = loc.getSensorDataSet();
+ Element positionTag = getChildElement(tag, "Position");
+ assertEquals(
+ Double.toString(location.getLatitude()), getChildTextValue(positionTag, "LatitudeDegrees"));
+ assertEquals(Double.toString(location.getLongitude()),
+ getChildTextValue(positionTag, "LongitudeDegrees"));
- List bpm = getChildElements(tag, "HeartRateBpm", 1);
+ assertEquals(Double.toString(location.getAltitude()), getChildTextValue(tag, "AltitudeMeters"));
+ assertTrue(location.getSensorDataSet() != null);
+ Sensor.SensorDataSet sds = location.getSensorDataSet();
+
+ List heartRate = getChildElements(tag, "HeartRateBpm", 1);
assertEquals(Integer.toString(sds.getHeartRate().getValue()),
- getChildTextValue(bpm.get(0), "Value"));
+ getChildTextValue(heartRate.get(0), "Value"));
- List ext = getChildElements(tag, "Extensions", 1);
- List tpx = getChildElements(ext.get(0), "TPX", 1);
- assertEquals(Integer.toString(sds.getPower().getValue()),
- getChildTextValue(tpx.get(0), "Watts"));
+ List extensions = getChildElements(tag, "Extensions", 1);
+ List tpx = getChildElements(extensions.get(0), "TPX", 1);
+ assertEquals(
+ Integer.toString(sds.getCadence().getValue()), getChildTextValue(tpx.get(0), "RunCadence"));
+ assertEquals(
+ Integer.toString(sds.getPower().getValue()), getChildTextValue(tpx.get(0), "Watts"));
}
}