/* * Copyright (C) 2012 - 2013 jonas.oreland@gmail.com * * This program 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. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package org.runnerup.export.format; import android.annotation.TargetApi; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.location.Location; import android.os.Build; import android.util.Pair; import org.runnerup.common.util.Constants; import org.runnerup.common.util.Constants.DB; import org.runnerup.util.KXmlSerializer; import org.runnerup.view.FeedActivity; import org.runnerup.workout.Sport; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** * TCX - export an activity in TCX format * * @todo Handle pauses * @todo Other sports than running * * @author jonas.oreland@gmail.com * */ @TargetApi(Build.VERSION_CODES.FROYO) public class TCX { long mID = 0; SQLiteDatabase mDB = null; KXmlSerializer mXML = null; String notes = null; SimpleDateFormat simpleDateFormat = null; Sport sport = null; private boolean addGratuitousTrack = false; public TCX(SQLiteDatabase mDB) { this.mDB = mDB; simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } String formatTime(long time) { return simpleDateFormat.format(new Date(time)); } public String export(long activityId, Writer writer) throws IOException { Pair<String,Sport> res = exportWithSport(activityId, writer); return res.first; } /** * @param activityId * @param writer * @return TCX id * @throws IOException */ public Pair<String,Sport> exportWithSport(long activityId, Writer writer) throws IOException { String[] aColumns = { DB.ACTIVITY.NAME, DB.ACTIVITY.COMMENT, DB.ACTIVITY.START_TIME, DB.ACTIVITY.SPORT, DB.ACTIVITY.META_DATA }; Cursor cursor = mDB.query(DB.ACTIVITY.TABLE, aColumns, "_id = " + activityId, null, null, null, null); cursor.moveToFirst(); long startTime = cursor.getLong(2); // epoch try { mXML = new KXmlSerializer(); mXML.setOutput(writer); mXML.startDocument("UTF-8", true); mXML.startTag("", "TrainingCenterDatabase"); mXML.attribute("", "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); mXML.attribute("", "xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); mXML.attribute("", "xmlns:ext", "http://www.garmin.com/xmlschemas/ActivityExtension/v2"); mXML.attribute("", "xmlns", "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"); mXML.startTag("", "Activities"); mXML.startTag("", "Activity"); if (cursor.isNull(3)) { mXML.attribute("", "Sport", "Running"); } else { // TCX supports only these 3 sports...(cf http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd) sport = Sport.valueOf(cursor.getInt(3)); if (sport.IsRunning()) { mXML.attribute("", "Sport", "Running"); } else if (sport.IsCycling()) { mXML.attribute("", "Sport", "Biking"); } else { mXML.attribute("", "Sport", "Other"); } } mXML.startTag("", "Id"); String id = formatTime(startTime * 1000); mXML.text(id); mXML.endTag("", "Id"); exportLaps(activityId, startTime * 1000, sport); if (!cursor.isNull(1)) { notes = cursor.getString(1); mXML.startTag("", "Notes"); mXML.text(notes); mXML.endTag("", "Notes"); } mXML.startTag("", "Creator"); mXML.attribute("", "xsi:type", "Device_t"); mXML.startTag("", "Name"); String creator = "RunnerUp " + android.os.Build.MODEL; if (!cursor.isNull(4)) { String metaData = cursor.getString(4); if (metaData.contains(DB.ACTIVITY.WITH_BAROMETER)) { creator += " with barometer"; } } mXML.text(creator); mXML.endTag("", "Name"); mXML.endTag("", "Creator"); mXML.endTag("", "Activity"); mXML.endTag("", "Activities"); mXML.endTag("", "TrainingCenterDatabase"); mXML.flush(); mXML.endDocument(); mXML = null; cursor.close(); return new Pair<String, Sport>(id, sport); } catch (IOException e) { cursor.close(); mXML = null; throw e; } } private void exportLaps(long activityId, long startTime, Sport sport) throws IOException { String[] lColumns = { DB.LAP.LAP, DB.LAP.DISTANCE, DB.LAP.TIME, DB.LAP.INTENSITY }; Cursor cLap = mDB.query(DB.LAP.TABLE, lColumns, DB.LAP.DISTANCE + " > 0 and " + DB.LAP.ACTIVITY + " = " + activityId, null, null, null, null); String[] pColumns = { DB.LOCATION.LAP, DB.LOCATION.TIME, DB.LOCATION.LATITUDE, DB.LOCATION.LONGITUDE, DB.LOCATION.ALTITUDE, DB.LOCATION.TYPE, DB.LOCATION.HR, DB.LOCATION.CADENCE, DB.LOCATION.TEMPERATURE, DB.LOCATION.PRESSURE }; Cursor cLocation = mDB.query(DB.LOCATION.TABLE, pColumns, DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null, null); boolean lok = cLap.moveToFirst(); boolean pok = cLocation.moveToFirst(); float totalDistance = 0; while (lok) { if (cLap.getFloat(1) != 0 && cLap.getLong(2) != 0) { long lap = cLap.getLong(0); while (pok && cLocation.getLong(0) != lap) { pok = cLocation.moveToNext(); } mXML.startTag("", "Lap"); if (pok && cLocation.getLong(0) == lap) { mXML.attribute("", "StartTime", formatTime(cLocation.getLong(1))); } else { mXML.attribute("", "StartTime", formatTime(startTime)); } mXML.startTag("", "TotalTimeSeconds"); mXML.text("" + cLap.getLong(2)); mXML.endTag("", "TotalTimeSeconds"); mXML.startTag("", "DistanceMeters"); mXML.text("" + cLap.getFloat(1)); mXML.endTag("", "DistanceMeters"); mXML.startTag("", "Calories"); mXML.text("0"); mXML.endTag("", "Calories"); mXML.startTag("", "Intensity"); mXML.text("Active"); mXML.endTag("", "Intensity"); mXML.startTag("", "TriggerMethod"); mXML.text("Manual"); mXML.endTag("", "TriggerMethod"); int maxHR = 0; long sumHR = 0; long cntHR = 0; int cntTrackpoints = 0; if (pok && cLocation.getLong(0) == lap) { mXML.startTag("", "Track"); float last_lat = 0; float last_longi = 0; long last_time = 0; while (pok && cLocation.getLong(0) == lap) { long time = cLocation.getLong(1); float lat = cLocation.getFloat(2); float longi = cLocation.getFloat(3); if (!(time == last_time && lat == last_lat && longi != last_longi)) { cntTrackpoints++; mXML.startTag("", "Trackpoint"); mXML.startTag("", "Time"); mXML.text(formatTime(time)); mXML.endTag("", "Time"); mXML.startTag("", "Position"); mXML.startTag("", "LatitudeDegrees"); mXML.text("" + lat); mXML.endTag("", "LatitudeDegrees"); mXML.startTag("", "LongitudeDegrees"); mXML.text("" + longi); mXML.endTag("", "LongitudeDegrees"); mXML.endTag("", "Position"); if (!cLocation.isNull(4)) { mXML.startTag("", "AltitudeMeters"); mXML.text("" + cLocation.getLong(4)); mXML.endTag("", "AltitudeMeters"); } float d[] = { 0 }; if (!(last_lat == 0 && last_longi == 0)) { Location.distanceBetween(last_lat, last_longi, lat, longi, d); } totalDistance += d[0]; mXML.startTag("", "DistanceMeters"); mXML.text("" + totalDistance); mXML.endTag("", "DistanceMeters"); if (!cLocation.isNull(6)) { int hr = cLocation.getInt(6); if (hr > 0) { maxHR = hr > maxHR ? hr : maxHR; sumHR += hr; cntHR++; mXML.startTag("", "HeartRateBpm"); mXML.startTag("", "Value"); String bpm = Integer.toString(hr); mXML.text(bpm); mXML.endTag("", "Value"); mXML.endTag("", "HeartRateBpm"); } } boolean isCad = !cLocation.isNull(7); boolean isBikeCad = isCad && sport.IsCycling(); boolean isRunCad = isCad && !isBikeCad; //Not supported in .tcx, uncomment for testing //boolean isTemp = !cLocation.isNull(8); //boolean isPres = !cLocation.isNull(9); //boolean isAnyExt = isRunCad || isTemp || isPres; if (isBikeCad) { int val = cLocation.getInt(7); mXML.startTag("", "Cadence"); String sval = Integer.toString(val); mXML.text(sval); mXML.endTag("", "Cadence"); } if (isRunCad) { mXML.startTag("", "Extensions"); mXML.startTag("", "TPX"); mXML.attribute("", "xmlns", "http://www.garmin.com/xmlschemas/ActivityExtension/v2"); //"standard" extensions: RunCadence, Speed, Watts } if (isRunCad) { int val = cLocation.getInt(7); mXML.startTag("", "RunCadence"); String sval = Integer.toString(val); mXML.text(sval); mXML.endTag("", "RunCadence"); // Not including "CadenceSensor Footpod" etc } //if (isTemp || isPres) { // if (isTemp) { // int val = cLocation.getInt(8); // mXML.startTag("", "ext:Temperature"); // String sval = Float.toString(val); // mXML.text(sval); // mXML.endTag("", "ext:Temperature"); // } // if (isPres) { // int val = cLocation.getInt(9); // mXML.startTag("", "ext:Pressure"); // String sval = Float.toString(val); // mXML.text(sval); // mXML.endTag("", "ext:Pressure"); // } //} if (isRunCad) { mXML.endTag("", "TPX"); mXML.endTag("", "Extensions"); } mXML.endTag("", "Trackpoint"); last_time = time; last_lat = lat; last_longi = longi; } pok = cLocation.moveToNext(); } mXML.endTag("", "Track"); } // Digifit chokes if there isn't at least *1* trackpoint, but is // ok // even if it's empty. if (cntTrackpoints == 0 && addGratuitousTrack) { mXML.startTag("", "Track"); mXML.startTag("", "Trackpoint"); mXML.startTag("", "Time"); mXML.text(formatTime(startTime)); mXML.endTag("", "Time"); mXML.endTag("", "Trackpoint"); mXML.endTag("", "Track"); } if (cntHR > 0) { mXML.startTag("", "AverageHeartRateBpm"); mXML.startTag("", "Value"); mXML.text(Integer.toString((int) (sumHR / cntHR))); mXML.endTag("", "Value"); mXML.endTag("", "AverageHeartRateBpm"); mXML.startTag("", "MaximumHeartRateBpm"); mXML.startTag("", "Value"); mXML.text(Integer.toString(maxHR)); mXML.endTag("", "Value"); mXML.endTag("", "MaximumHeartRateBpm"); } mXML.endTag("", "Lap"); } lok = cLap.moveToNext(); } cLap.close(); cLocation.close(); } public String getNotes() { return notes; } public Sport getSport() { return sport; } public void setAddGratuitousTrack(boolean addGratuitousTrack) { this.addGratuitousTrack = addGratuitousTrack; } }