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(" "); - } - } - - @Override - public void writeLocation(Location location) { - if (pw == null) { - return; - } - pw.println(" "); - Date d = new Date(location.getTime()); - pw.println(" "); - pw.println(" "); - - pw.print(" "); - pw.print(location.getLatitude()); - pw.println(""); - - pw.print(" "); - pw.print(location.getLongitude()); - pw.println(""); - - pw.println(" "); - pw.print(" "); - pw.print(location.getAltitude()); - pw.println(""); - - 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(" "); - pw.print(""); - pw.print(sensorData.getHeartRate().getValue()); - pw.print(""); - pw.println(""); - } - - boolean cadenceAvailable = sensorData.hasCadence() - && sensorData.getCadence().getState() == Sensor.SensorState.SENDING - && sensorData.getCadence().hasValue(); - - // TCX Trackpoint_t contains a sequence. Thus, the legacy XML element - // needs to be put before . - // 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 as . - if (sportIsCycling && cadenceAvailable) { - pw.print(" "); - pw.print(Math.min(254, sensorData.getCadence().getValue())); - pw.println(""); - } - - boolean powerAvailable = sensorData.hasPower() - && sensorData.getPower().getState() == Sensor.SensorState.SENDING - && sensorData.getPower().hasValue(); - - if(powerAvailable || (!sportIsCycling && cadenceAvailable)) { - pw.print(" "); - pw.print(""); - - // RunCadence needs to be put before power in order to be understood - // by Garmin Training Center. - if (!sportIsCycling && cadenceAvailable) { - pw.print(""); - pw.print(Math.min(254, sensorData.getCadence().getValue())); - pw.print(""); - } - - if (powerAvailable) { - pw.print(""); - pw.print(sensorData.getPower().getValue()); - pw.print(""); - } - - pw.println(""); - } - } - } - pw.println(" "); - } - - @Override - public void writeCloseSegment() { - 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(" "); + } - // 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("My Tracks %s by Google\n", SystemUtils.getMyTracksVersion(context)); + @Override + public void writeCloseSegment() { + 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")); } }