/* * Copyright (C) 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 org.runnerup.common.util.Constants.DB; import org.runnerup.util.Formatter; import org.runnerup.util.KXmlSerializer; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Vector; /** * @author jonas.oreland@gmail.com * */ @TargetApi(Build.VERSION_CODES.FROYO) public class NikeXML { private static final String DEVICE = "iPod"; SQLiteDatabase mDB = null; KXmlSerializer mXML = null; SimpleDateFormat simpleDateFormat = null; public NikeXML(final SQLiteDatabase db) { mDB = db; simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); } private String formatTime(final long time) { final String s = simpleDateFormat.format(new Date(time)); return String.format("%s:%s", s.substring(0, 22), s.substring(22)); // plain // weird! } enum Dim { DISTANCE, LAP, TIME, SPEED, HR } public void export(final long activityId, final Writer writer) throws Exception { final String[] aColumns = { DB.ACTIVITY.NAME, DB.ACTIVITY.COMMENT, DB.ACTIVITY.START_TIME, DB.ACTIVITY.DISTANCE, DB.ACTIVITY.TIME, DB.ACTIVITY.SPORT }; final Cursor cursor = mDB.query(DB.ACTIVITY.TABLE, aColumns, "_id = " + activityId, null, null, null, null); cursor.moveToFirst(); final long startTime = cursor.getLong(2) * 1000; // epoch final double distance = cursor.getDouble(3); final long duration = cursor.getLong(4); try { mXML = new KXmlSerializer(); mXML.setFeature( "http://xmlpull.org/v1/doc/features.html#indent-output", true); mXML.setOutput(writer); mXML.startDocument("UTF-8", true); mXML.startTag("", "sportsData"); mXML.startTag("", "runSummary"); mXML.startTag("", "time"); mXML.text(formatTime(startTime)); mXML.endTag("", "time"); mXML.startTag("", "duration"); mXML.text(Long.toString(duration * 1000)); // in ms mXML.endTag("", "duration"); mXML.startTag("", "distance"); mXML.attribute("", "unit", "km"); mXML.text(Double.toString(distance / 1000)); // in km mXML.endTag("", "distance"); mXML.startTag("", "calories"); mXML.text("0"); mXML.endTag("", "calories"); mXML.startTag("", "battery"); mXML.endTag("", "battery"); final boolean hasHR = emitHeartrateStats(activityId); mXML.endTag("", "runSummary"); mXML.startTag("", "template"); mXML.startTag("", "templateName"); mXML.cdsect("Basic"); mXML.endTag("", "templateName"); mXML.endTag("", "template"); mXML.startTag("", "goal"); mXML.attribute("", "type", ""); mXML.attribute("", "unit", ""); mXML.attribute("", "value", ""); mXML.endTag("", "goal"); mXML.startTag("", "userInfo"); mXML.startTag("", "empedID"); mXML.text("XXXXXXXXXXX"); mXML.endTag("", "empedID"); mXML.startTag("", "weight"); mXML.endTag("", "weight"); mXML.startTag("", "device"); mXML.text(DEVICE); mXML.endTag("", "device"); mXML.startTag("", "calibration"); mXML.endTag("", "calibration"); mXML.endTag("", "userInfo"); mXML.startTag("", "startTime"); mXML.text(formatTime(startTime)); mXML.endTag("", "startTime"); mXML.startTag("", "snapShotList"); mXML.attribute("", "snapShotType", "kmSplit"); emitList(activityId, Dim.DISTANCE, 1000d, new SnapshotList()); mXML.endTag("", "snapShotList"); mXML.startTag("", "snapShotList"); mXML.attribute("", "snapShotType", "mileSplit"); emitList(activityId, Dim.DISTANCE, Formatter.mi_meters, new SnapshotList()); mXML.endTag("", "snapShotList"); mXML.startTag("", "snapShotList"); mXML.attribute("", "snapShotType", "userClick"); emitList(activityId, Dim.LAP, 1, new SnapshotList("onDemandVP")); mXML.endTag("", "snapShotList"); mXML.startTag("", "extendedDataList"); { mXML.startTag("", "extendedData"); mXML.attribute("", "dataType", "distance"); mXML.attribute("", "intervalType", "time"); mXML.attribute("", "intervalUnit", "s"); mXML.attribute("", "intervalValue", "10"); final ExtendedData e = new ExtendedData(Dim.DISTANCE); e.buf.append("0.0"); emitList(activityId, Dim.TIME, 10 * 1000d, e); mXML.text(e.buf.toString()); mXML.endTag("", "extendedData"); } { mXML.startTag("", "extendedData"); mXML.attribute("", "dataType", "speed"); mXML.attribute("", "intervalType", "time"); mXML.attribute("", "intervalUnit", "s"); mXML.attribute("", "intervalValue", "10"); final ExtendedData e = new ExtendedData(Dim.SPEED); e.buf.append("0.0"); emitList(activityId, Dim.TIME, 10 * 1000d, e); mXML.text(e.buf.toString()); mXML.endTag("", "extendedData"); } if (hasHR) { mXML.startTag("", "extendedData"); mXML.attribute("", "dataType", "heartRate"); mXML.attribute("", "intervalType", "time"); mXML.attribute("", "intervalUnit", "s"); mXML.attribute("", "intervalValue", "10"); final ExtendedData e = new ExtendedData(Dim.HR); e.buf.append("0"); emitList(activityId, Dim.TIME, 10 * 1000d, e); mXML.text(e.buf.toString()); mXML.endTag("", "extendedData"); } mXML.endTag("", "extendedDataList"); mXML.endTag("", "sportsData"); mXML.endDocument(); mXML.flush(); } catch (final Exception e) { throw e; } cursor.close(); } private boolean emitHeartrateStats(long mID) throws IllegalArgumentException, IllegalStateException, IOException { String args[] = { Long.toString(mID) }; Cursor c = mDB.rawQuery("select min(" + DB.LOCATION.HR + "), max(" + DB.LOCATION.HR + "), avg(" + DB.LOCATION.HR + ") FROM " + DB.LOCATION.TABLE + " WHERE " + DB.LOCATION.ACTIVITY + " = ?", args); if (c.moveToFirst() && !(c.isNull(0) || c.isNull(1) || c.isNull(2))) { int minHR = c.getInt(0); int maxHR = c.getInt(1); int avgHR = c.getInt(2); c.close(); mXML.startTag("", "heartrate"); mXML.startTag("", "average"); mXML.text(Integer.toString(avgHR)); mXML.endTag("", "average"); emitHRPosition(mID, "minimum", minHR); emitHRPosition(mID, "maximum", maxHR); mXML.endTag("", "heartrate"); return true; } c.close(); return false; } private void emitHRPosition(long mID, String string, int hrVal) throws IllegalArgumentException, IllegalStateException, IOException { long _id = 0; { // 1 find a point with specified value String args[] = { Long.toString(mID), Integer.toString(hrVal) }; Cursor c = mDB.rawQuery( "select min(_id) from location where activity_id = ? and hr = ? limit 1", args); if (!c.moveToFirst()) { c.close(); return; } _id = c.getLong(0); c.close(); } // 2 iterate to that position from start... String cols[] = { DB.LOCATION.TYPE, DB.LOCATION.LATITUDE, DB.LOCATION.LONGITUDE, DB.LOCATION.TIME, DB.LOCATION.SPEED }; String args[] = { Long.toString(mID), Long.toString(_id) }; Cursor c = mDB.query(DB.LOCATION.TABLE, cols, DB.LOCATION.ACTIVITY + " = ? AND _id <= ?", args, null, null, "_id"); if (c.moveToFirst()) { Location last = null; double sumDist = 0; long sumTime = 0; do { switch (c.getInt(0)) { case DB.LOCATION.TYPE_START: case DB.LOCATION.TYPE_RESUME: last = new Location("Dill"); last.setLatitude(c.getDouble(1)); last.setLongitude(c.getDouble(2)); last.setTime(c.getLong(3)); break; case DB.LOCATION.TYPE_PAUSE: case DB.LOCATION.TYPE_END: last = null; break; case DB.LOCATION.TYPE_GPS: Location l = new Location("Sill"); l.setLatitude(c.getDouble(1)); l.setLongitude(c.getDouble(2)); l.setTime(c.getLong(3)); if (!c.isNull(4)) l.setSpeed(c.getFloat(4)); sumDist += l.distanceTo(last); sumTime += l.getTime() - last.getTime(); last = l; } } while (c.moveToNext()); mXML.startTag("", string); mXML.startTag("", "duration"); mXML.text(Long.toString(sumTime)); // ms mXML.endTag("", "duration"); mXML.startTag("", "distance"); mXML.text(Double.toString(sumDist / 1000.0d)); // km mXML.endTag("", "distance"); mXML.startTag("", "pace"); double pace = 0; if (last != null && last.hasSpeed() && last.getSpeed() != 0) { pace = 1000.0d / last.getSpeed(); } else { if (sumDist != 0) pace = sumTime / sumDist; } mXML.text(Long.toString(Math.round(1000.0d * pace))); mXML.endTag("", "pace"); mXML.startTag("", "bpm"); mXML.text(Integer.toString(hrVal)); mXML.endTag("", "bpm"); mXML.endTag("", string); } c.close(); } abstract class Emitter { public abstract void emit(Pos p, Vector<Pos> posHist, Vector<Location> hist) throws Exception; } class SnapshotList extends Emitter { String event = null; public SnapshotList() { } public SnapshotList(final String evnt) { event = evnt; } @Override public void emit(final Pos p, final Vector<Pos> posHist, final Vector<Location> hist) throws Exception { mXML.startTag("", "snapShot"); if (event != null) mXML.attribute("", "event", event); mXML.startTag("", "duration"); mXML.text(Long.toString(p.sumTime)); mXML.endTag("", "duration"); mXML.startTag("", "distance"); mXML.text(Double.toString(Math.round(1000.0 * p.sumDistance / 1000.0) / 1000.0d)); mXML.endTag("", "distance"); mXML.startTag("", "pace"); double deltaTime = p.sumTime; double deltaDist = p.sumDistance; double deltaHR = p.sumHR; if (!posHist.isEmpty()) { deltaTime -= posHist.lastElement().sumTime; deltaDist -= posHist.lastElement().sumDistance; deltaHR -= posHist.lastElement().sumHR; } double pace = 0; if (deltaDist != 0) { pace = 1000d * deltaTime / deltaDist; } mXML.text(Long.toString(Math.round(pace))); mXML.endTag("", "pace"); if (deltaHR > 0 && deltaTime > 0) { double avgHR = deltaHR / deltaTime; mXML.startTag("", "bpm"); mXML.text(Long.toString(Math.round(avgHR))); mXML.endTag("", "bpm"); } mXML.endTag("", "snapShot"); } } class ExtendedData extends Emitter { Dim d = null; final StringBuffer buf = new StringBuffer(); public ExtendedData(final Dim dim) { d = dim; } @Override public void emit(final Pos p, final Vector<Pos> posHist, final Vector<Location> hist) throws Exception { if (d == Dim.DISTANCE) { buf.append(' '); buf.append(Double.toString(Math.round(1000.0 * p.sumDistance / 1000.0) / 1000.0d)); } else if (d == Dim.SPEED) { double deltaTime = p.sumTime; double deltaDist = p.sumDistance; if (!posHist.isEmpty()) { deltaTime -= posHist.lastElement().sumTime; deltaDist -= posHist.lastElement().sumDistance; } double speed = 0; // km/h if (deltaTime != 0) { speed = deltaDist / deltaTime; } buf.append(' '); buf.append(Double.toString(speed)); } else if (d == Dim.HR) { double deltaTime = p.sumTime; double deltaHR = p.sumHR; if (!posHist.isEmpty()) { deltaTime -= posHist.lastElement().sumTime; deltaHR -= posHist.lastElement().sumHR; } double avgHR = 0; if (deltaTime != 0) { avgHR = deltaHR / deltaTime; } buf.append(' '); buf.append(Long.toString(Math.round(avgHR))); } } } class Pos { public Pos() { } public Pos(final Pos p) { sumTime = p.sumTime; sumDistance = p.sumDistance; sumHR = p.sumHR; } long sumTime = 0; double sumDistance = 0; long sumHR = 0; } private void emitList(final long activityId, final Dim d, final double add, final Emitter out) throws Exception { double first = add; final String[] pColumns = { DB.LOCATION.LAP, // 0 DB.LOCATION.TIME, // 1 DB.LOCATION.LATITUDE, // 2 DB.LOCATION.LONGITUDE,// 3 DB.LOCATION.ALTITUDE, // 4 DB.LOCATION.TYPE, // 5 DB.LOCATION.HR }; // 6 final Cursor c = mDB.query(DB.LOCATION.TABLE, pColumns, DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null, null); try { final Pos p = new Pos(); int lastLap = 0; final Vector<Location> locHist = new Vector<Location>(); final Vector<Pos> posHist = new Vector<Pos>(); if (c.moveToFirst()) { do { final int type = c.getInt(5); if (type == DB.LOCATION.TYPE_RESUME) { locHist.clear(); continue; } final Location l = new Location("Sill E Dill"); final int lap = c.getInt(0); l.setTime(c.getLong(1)); l.setLatitude(c.getDouble(2)); l.setLongitude(c.getDouble(3)); l.setProvider("" + c.getLong(3)); long hr = 0; if (!c.isNull(6)) { hr = c.getLong(6); } long deltaTime = 0; double deltaDist = 0; double bearing = 0; if (!locHist.isEmpty()) { deltaTime = l.getTime() - locHist.lastElement().getTime(); deltaDist = l.distanceTo(locHist.lastElement()); bearing = locHist.lastElement().bearingTo(l); } while ((d == Dim.DISTANCE && p.sumDistance + deltaDist >= first) || (d == Dim.TIME && p.sumTime + deltaTime >= first)) { double diffTime = 0; double diffDist = 0; double pct = 0; if (d == Dim.DISTANCE) { diffDist = first - p.sumDistance; pct = diffDist / deltaDist; diffTime = deltaTime * pct; } else { diffTime = first - p.sumTime; pct = diffTime / deltaTime; diffDist = deltaDist * pct; } final Location tmp = new Location(locHist.lastElement()); move(tmp, bearing, diffDist); // move location tmp.setTime((long) (tmp.getTime() + diffTime)); // move // time locHist.add(tmp); p.sumDistance += diffDist; p.sumTime += diffTime; p.sumHR += diffTime * hr; out.emit(p, posHist, locHist); posHist.add(new Pos(p)); locHist.remove(locHist.size() - 1); // remove synthetic // location deltaTime -= diffTime; deltaDist -= diffDist; first += add; } if (d == Dim.LAP && lastLap != lap) { lastLap = lap; if ((posHist.isEmpty() && p.sumTime > 0 && p.sumDistance > 0) || (!posHist.isEmpty() && p.sumTime > posHist.lastElement().sumTime && p.sumDistance > posHist .lastElement().sumDistance)) { out.emit(p, posHist, locHist); posHist.add(new Pos(p)); } else { continue; } } locHist.add(l); if (locHist.size() == 6) { locHist.remove(0); } p.sumTime += deltaTime; p.sumDistance += deltaDist; p.sumHR += hr * deltaTime; } while (c.moveToNext()); } } finally { if (c != null) c.close(); } } static final double R = 6371.0; // radius of earth in km /** * Move a location <em>dist</em> meters in direction of <em>bearing</em> * * @param l * @param bearing * @param dist * @see <url>http://www.movable-type.co.uk/scripts/latlong.html</url> */ public static void move(final Location l, final double bearing, final double dist) { final double d = dist / 1000.0; // in km final double lat1 = Math.toRadians(l.getLatitude()); final double lon1 = Math.toRadians(l.getLongitude()); final double brng = Math.toRadians(bearing); final double lat2 = Math.asin(Math.sin(lat1) * Math.cos(d / R) + Math.cos(lat1) * Math.sin(dist / R) * Math.cos(brng)); final double lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d / R) * Math.cos(lat1), Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2)); l.setLatitude(Math.toDegrees(lat2)); l.setLongitude(Math.toDegrees(lon2)); } }