/* This file is part of Wattzap Community Edition.
*
* Wattzap Community Edtion is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Wattzap Community Edition is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Wattzap. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wattzap.utils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.wattzap.model.RouteReader;
import com.wattzap.model.UserPreferences;
import com.wattzap.model.dto.Point;
import com.wattzap.model.dto.Telemetry;
/**
* 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.
*
* @author Sandor Dornbush
* @author David George
*/
public class TcxWriter /* implements TrackWriter */{
protected static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
protected static final String FILE_TIMESTAMP_FORMAT = "yyyyMMMdd-HHmmss";
public final static String WORKOUTDIR = "/Workouts/";
// 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";
RouteReader routeData;
// 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 final static SimpleDateFormat fileTSFormatter = new SimpleDateFormat(
FILE_TIMESTAMP_FORMAT);
private PrintWriter pw = null;
private static Logger logger = LogManager.getLogger("TCX Writer");
public TcxWriter() {
timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
public void writeHeader() {
if (pw == null) {
return;
}
pw.format("<?xml version=\"1.0\" encoding=\"UTF8\" standalone=\"no\" ?>\n");
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();
}
public void writeStartTrack(Point start, Point end) {
if (pw == null) {
return;
}
String startTime = timestampFormatter.format(new Date(start.getTime()));
pw.println(" <Activities>");
pw.format(" <Activity Sport=\"%s\">\n", TCX_SPORT_BIKING);
pw.format(" <Id>%s</Id>\n", startTime);
pw.format(" <Lap StartTime=\"%s\">\n", startTime);
pw.print(" <TotalTimeSeconds>");
pw.print((end.getTime() - start.getTime()) / 1000);
pw.println("</TotalTimeSeconds>");
pw.print(" <DistanceMeters>");
pw.print(end.getDistanceFromStart());
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>");
}
public void writeOpenSegment() {
if (pw != null) {
pw.println(" <Track>");
}
}
public void writeLocation(Point t, int gpsData) {
if (pw == null) {
return;
}
pw.println(" <Trackpoint>");
Date d = new Date(t.getTime());
pw.println(" <Time>" + timestampFormatter.format(d)
+ "</Time>");
if (gpsData == 0) {
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
pw.print(t.getLatitude());
pw.println("</LatitudeDegrees>");
pw.print(" <LongitudeDegrees>");
pw.print(t.getLongitude());
pw.println("</LongitudeDegrees>");
pw.println(" </Position>");
pw.print(" <AltitudeMeters>");
pw.print(t.getElevation());
pw.println("</AltitudeMeters>");
}
pw.print(" <DistanceMeters>");
pw.print(t.getDistanceFromStart());
pw.println("</DistanceMeters>");
pw.print(t.getTcxExtensionsXml());;
pw.println(" </Trackpoint>");
}
void writeCloseSegment() {
if (pw != null) {
pw.println(" </Track>");
}
}
public void writeEndTrack() {
if (pw == null) {
return;
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>Wattzap Analyzer running on %s</Name>\n", "2.0.0");
// 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>");
}
public void writeFooter() {
if (pw == null) {
return;
}
pw.println(" <Author xsi:type=\"Application_t\">");
// 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",
pw.println("<Build>");
writeVersion();
pw.format("<Type>%s</Type>\n", TCX_TYPE_RELEASE);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>");
pw.println("</TrainingCenterDatabase>");
}
/*
* @Override public String getExtension() { return
* TrackFileFormat.TCX.getExtension(); }
*/
private void writeVersion() {
if (pw == null) {
return;
}
String version = "1.1.1";
// 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 = version.split("\\.");
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", 1);
pw.println("</Version>");
}
/**
*
* @param data
* @param gpsData
* 0 - save GPS data, 1 - drop GPS data
* @return
*/
public String save(List<? extends Point> data, int gpsData) {
String fileName = null;
if (data == null || data.size() == 0) {
logger.info("No training data to save");
return fileName;
}
Point firstPoint = data.get(0);
Point lastPoint = data.get(data.size() - 1);
fileName = getWorkoutName(firstPoint.getTime());
File file = new File(UserPreferences.INSTANCE.getUserDataDirectory()
+ WORKOUTDIR + fileName);
try {
// make sure parent directory exists
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
file.createNewFile();
pw = new PrintWriter(file);
writeHeader();
writeStartTrack(firstPoint, lastPoint);
writeOpenSegment();
Point last = null;
for (Point t : data) {
if (t.getLatitude() > 90 || t.getLongitude() > 180) {
// No GPS data to save
writeLocation(t, 1);
} else {
if (gpsData == 0 && last != null
&& last.getLatitude() == t.getLatitude()
&& last.getLongitude() == t.getLongitude()
&& t.getLatitude() != 0 && t.getLongitude() != 0) {
/*
* We are saving GPS data, it is the same GPS Point and
* the data is valid (not 0,0), drop it.
*/
continue;
} else {
writeLocation(t, gpsData);
}
}
last = t;
}
writeCloseSegment();
writeEndTrack();
writeFooter();
pw.flush();
} catch (FileNotFoundException e1) {
logger.error(e1.getLocalizedMessage() + " "
+ file.getAbsolutePath());
} catch (IOException e1) {
logger.error(e1.getLocalizedMessage() + " "
+ file.getAbsolutePath());
} finally {
close();
}
return fileName;
}
public static String getWorkoutName(long time) {
Calendar date = (new GregorianCalendar());
date.setTimeInMillis(time);
int season = date.get(Calendar.YEAR);
String workoutName = fileTSFormatter.format(new Date(time));
return season + "/" + workoutName + ".tcx";
}
}