Fix TCX export

This commit is contained in:
Matthew Simmons
2011-03-16 14:04:53 -04:00
parent 65f44da4c9
commit c943e5dd43
3 changed files with 149 additions and 61 deletions
@@ -17,10 +17,11 @@ package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import android.location.Location; import android.location.Location;
import android.os.Build; import android.os.Build;
@@ -30,6 +31,7 @@ import java.io.PrintWriter;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
/** /**
@@ -37,20 +39,32 @@ import java.util.TimeZone;
* As defined by: * As defined by:
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 * 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.
*
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class TcxTrackWriter implements TrackFormatWriter { public class TcxTrackWriter implements TrackFormatWriter {
protected static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
// 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 SimpleDateFormat timestampFormatter;
private PrintWriter pw = null; private PrintWriter pw = null;
private Track track; private Track track;
static final SimpleDateFormat TIMESTAMP_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static {
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public TcxTrackWriter() { public TcxTrackWriter() {
timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
} }
@Override @Override
@@ -93,16 +107,13 @@ public class TcxTrackWriter implements TrackFormatWriter {
if (pw == null) { if (pw == null) {
return; return;
} }
String startTime = timestampFormatter.format(track.getStatistics().getStartTime());
pw.println(" <Activities>"); pw.println(" <Activities>");
pw.print(" <Activity Sport=\""); pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory()));
if (track.getCategory() != null) { pw.format(" <Id>%s</Id>\n", startTime);
pw.print(track.getCategory()); pw.format(" <Lap StartTime=\"%s\">\n", startTime);
}
pw.println("\">");
pw.print(" <Id>");
pw.print(TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime()));
pw.println("</Id>");
pw.println(" <Lap>");
pw.print(" <TotalTimeSeconds>"); pw.print(" <TotalTimeSeconds>");
pw.print(track.getStatistics().getTotalTime() / 1000); pw.print(track.getStatistics().getTotalTime() / 1000);
pw.println("</TotalTimeSeconds>"); pw.println("</TotalTimeSeconds>");
@@ -113,7 +124,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
// Calories are a required element just put in 0. // Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>"); pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>"); pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>)"); pw.println("<TriggerMethod>Manual</TriggerMethod>");
} }
@Override @Override
@@ -130,7 +141,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
} }
pw.println(" <Trackpoint>"); pw.println(" <Trackpoint>");
Date d = new Date(location.getTime()); Date d = new Date(location.getTime());
pw.println(" <Time>" + TIMESTAMP_FORMAT.format(d) + "</Time>"); pw.println(" <Time>" + timestampFormatter.format(d) + "</Time>");
pw.println(" <Position>"); pw.println(" <Position>");
pw.print(" <LatitudeDegrees>"); pw.print(" <LatitudeDegrees>");
@@ -194,10 +205,18 @@ public class TcxTrackWriter implements TrackFormatWriter {
} }
pw.println(" </Lap>"); pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">"); pw.print(" <Creator xsi:type=\"Device_t\">");
pw.print("<Name>"); pw.format("<Name>%s</Name>\n", Build.MODEL);
pw.print(Build.MODEL);
pw.print("</Name>"); // The following code is correct. ID is inconsistently capitalized in the
pw.println("</Creator>)"); // TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
pw.println("<Version>");
pw.println("<VersionMajor>0</VersionMajor>");
pw.println("<VersionMinor>0</VersionMinor>");
pw.println("</Version>");
pw.println("</Creator>");
pw.println(" </Activity>"); pw.println(" </Activity>");
pw.println(" </Activities>"); pw.println(" </Activities>");
} }
@@ -207,8 +226,24 @@ public class TcxTrackWriter implements TrackFormatWriter {
if (pw == null) { if (pw == null) {
return; return;
} }
pw.print(" <Author xsi:type=\"Application_t\">"); pw.println(" <Author xsi:type=\"Application_t\">");
pw.print("<Name>My Tracks by Google</Name>");
// We put the version in the name because there isn't a better place for
// it. The TCX schema tightly defined the Version tag, so we can't put it
// there. They've similarly constrained the PartNumber tag, so it can't go
// there either.
pw.format("<Name>My Tracks %s by Google</Name>\n", MyTracksUtils.getMyTracksVersion());
pw.println("<Build>");
pw.println("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", MyTracksUtils.getMyTracksVersionCode());
pw.println("<VersionMinor>0</VersionMinor>");
pw.println("</Version>");
pw.format("<Type>%s</Type>\n", MyTracksUtils.isRelease() ? TCX_TYPE_RELEASE
: TCX_TYPE_INTERNAL);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>"); pw.println("</Author>");
pw.println("</TrainingCenterDatabase>"); pw.println("</TrainingCenterDatabase>");
} }
@@ -217,4 +252,14 @@ public class TcxTrackWriter implements TrackFormatWriter {
public void writeWaypoint(Waypoint waypoint) { public void writeWaypoint(Waypoint waypoint) {
// TODO Write out the waypoints somewhere. // TODO Write out the waypoints somewhere.
} }
private String categoryToTcxSport(String category) {
if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
return TCX_SPORT_RUNNING;
} else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
return TCX_SPORT_BIKING;
} else {
return TCX_SPORT_OTHER;
}
}
} }
@@ -15,10 +15,10 @@
*/ */
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.MyTracks;
import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
import android.app.Activity; import android.app.Activity;
@@ -259,34 +259,66 @@ public class MyTracksUtils {
* Returns whether or not this is a release build. * Returns whether or not this is a release build.
*/ */
public static boolean isRelease(Context context) { public static boolean isRelease(Context context) {
try { PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES);
Signature [] sigs = context.getPackageManager().getPackageInfo( if (packageInfo == null) {
context.getPackageName(), PackageManager.GET_SIGNATURES).signatures; return false;
for (Signature sig : sigs) {
if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) {
return true;
}
}
} catch (NameNotFoundException e) {
Log.e(MyTracksConstants.TAG, "Unable to get signatures", e);
} }
for (Signature sig: packageInfo.signatures) {
if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) {
return true;
}
}
return false; return false;
} }
/**
* @see isRelease(Context)
*/
public static boolean isRelease() {
return isRelease(MyTracks.getInstance());
}
/** /**
* Get the My Tracks version from the manifest. * Get the My Tracks version from the manifest.
* *
* @return the version, or an empty string in case of failure. * @return the version, or an empty string in case of failure.
*/ */
public static String getMyTracksVersion(Context context) { public static String getMyTracksVersion(Context context) {
PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_META_DATA);
return packageInfo == null ? "" : packageInfo.versionName;
}
/**
* @see getMyTracksVersion(Context)
*/
public static String getMyTracksVersion() {
return getMyTracksVersion(MyTracks.getInstance());
}
/**
* Return the My Tracks version code from the manifest.
*
* @return the version code, or an empty string in case of failure.
*/
public static int getMyTracksVersionCode() {
PackageInfo packageInfo = getPackageInfo(MyTracks.getInstance(),
PackageManager.GET_META_DATA);
return packageInfo == null ? -1 : packageInfo.versionCode;
}
private static PackageInfo getPackageInfo(Context context, int flags) {
if (context == null) {
Log.w(MyTracksConstants.TAG, "No context found when attempting to get version");
return null;
}
try { try {
PackageInfo pi = context.getPackageManager().getPackageInfo( return context.getPackageManager().getPackageInfo(
"com.google.android.maps.mytracks", "com.google.android.maps.mytracks", flags);
PackageManager.GET_META_DATA);
return pi.versionName;
} catch (NameNotFoundException e) { } catch (NameNotFoundException e) {
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e); Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
return ""; return null;
} }
} }
@@ -4,18 +4,29 @@ package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import java.util.Date;
import java.util.List;
/** /**
* Tests for the GPX track exporter. * Tests for the GPX track exporter.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class TcxTrackWriterTest extends TrackFormatWriterTest { public class TcxTrackWriterTest extends TrackFormatWriterTest {
private DateFormat timestampFormatter;
@Override
public void setUp() throws Exception {
super.setUp();
timestampFormatter = new SimpleDateFormat(TcxTrackWriter.TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public void testXmlOutput() throws Exception { public void testXmlOutput() throws Exception {
TrackFormatWriter writer = new TcxTrackWriter(); TrackFormatWriter writer = new TcxTrackWriter();
@@ -58,8 +69,8 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest {
getChildTextValue(posTag, "LatitudeDegrees")); getChildTextValue(posTag, "LatitudeDegrees"));
assertEquals(Double.toString(loc.getLongitude()), assertEquals(Double.toString(loc.getLongitude()),
getChildTextValue(posTag, "LongitudeDegrees")); getChildTextValue(posTag, "LongitudeDegrees"));
assertEquals(
TcxTrackWriter.TIMESTAMP_FORMAT.format(new Date(loc.getTime())), assertEquals(timestampFormatter.format(loc.getTime()),
getChildTextValue(tag, "Time")); getChildTextValue(tag, "Time"));
assertEquals(Double.toString(loc.getAltitude()), assertEquals(Double.toString(loc.getAltitude()),
getChildTextValue(tag, "AltitudeMeters")); getChildTextValue(tag, "AltitudeMeters"));