/*
* Copyright (C) 2012 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.os.Build;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.runnerup.common.util.Constants;
import org.runnerup.common.util.Constants.DB;
import org.runnerup.db.entities.ActivityEntity;
import org.runnerup.db.entities.LapEntity;
import org.runnerup.db.entities.LocationEntity;
import org.runnerup.export.RunKeeperSynchronizer;
import org.runnerup.util.JsonWriter;
import org.runnerup.workout.Sport;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
/**
* @author jonas.oreland@gmail.com
*
*/
@TargetApi(Build.VERSION_CODES.FROYO)
public class RunKeeper {
long mID = 0;
SQLiteDatabase mDB = null;
public RunKeeper(SQLiteDatabase db) {
mDB = db;
}
static String formatTime(long time) {
return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US)
.format(new Date(time));
}
public void export(long activityId, Writer writer) throws IOException {
ActivityEntity ae = new ActivityEntity();
ae.readByPrimaryKey(mDB, activityId);
long startTime = ae.getStartTime();
double distance = ae.getDistance();
long duration = ae.getTime();
String comment = ae.getComment();
try {
JsonWriter w = new JsonWriter(writer);
w.beginObject();
Sport s = Sport.valueOf(ae.getSport());
if (!RunKeeperSynchronizer.sport2runkeeperMap.containsKey(s)) {
s = Sport.OTHER;
}
w.name("type").value(RunKeeperSynchronizer.sport2runkeeperMap.get(s));
w.name("equipment").value("None");
w.name("start_time").value(formatTime(startTime * 1000));
w.name("total_distance").value(distance);
w.name("duration").value(duration);
if (comment != null && comment.length() > 0) {
w.name("notes").value(comment);
}
//it seems that upload fails if writting an empty array...
if (ae.getMaxHr()!=null) {
w.name("heart_rate");
w.beginArray();
exportHeartRate(activityId, startTime, w);
w.endArray();
}
exportPath("path", activityId, startTime, w);
w.name("post_to_facebook").value(false);
w.name("post_to_twitter").value(false);
w.endObject();
} catch (IOException e) {
throw e;
}
}
private void exportHeartRate(long activityId, long startTime, JsonWriter w)
throws IOException {
String[] pColumns = {
DB.LOCATION.TIME, DB.LOCATION.HR
};
Cursor cursor = mDB.query(DB.LOCATION.TABLE, pColumns,
DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null,
null);
if (cursor.moveToFirst()) {
startTime = cursor.getLong(0);
do {
if (!cursor.isNull(1)) {
w.beginObject();
w.name("timestamp").value(
(cursor.getLong(0) - startTime) / 1000);
w.name("heart_rate").value(Integer.toString(cursor.getInt(1)));
w.endObject();
}
} while (cursor.moveToNext());
}
cursor.close();
}
private void exportPath(String name, long activityId, long startTime, JsonWriter w)
throws IOException {
String[] pColumns = {
DB.LOCATION.TIME, DB.LOCATION.LATITUDE,
DB.LOCATION.LONGITUDE, DB.LOCATION.ALTITUDE, DB.LOCATION.TYPE
};
Cursor cursor = mDB.query(DB.LOCATION.TABLE, pColumns,
DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null,
null);
if (cursor.moveToFirst()) {
w.name(name);
w.beginArray();
startTime = cursor.getLong(0);
do {
w.beginObject();
w.name("timestamp").value(
(cursor.getLong(0) - startTime) / 1000);
w.name("latitude").value(cursor.getDouble(1));
w.name("longitude").value(cursor.getDouble(2));
if (!cursor.isNull(3)) {
w.name("altitude").value(cursor.getDouble(3));
}
if (cursor.getLong(4) == DB.LOCATION.TYPE_START) {
w.name("type").value("start");
} else if (cursor.getLong(4) == DB.LOCATION.TYPE_END) {
w.name("type").value("end");
} else if (cursor.getLong(4) == DB.LOCATION.TYPE_PAUSE) {
w.name("type").value("pause");
} else if (cursor.getLong(4) == DB.LOCATION.TYPE_RESUME) {
w.name("type").value("resume");
} else if (cursor.getLong(4) == DB.LOCATION.TYPE_GPS) {
w.name("type").value("gps");
} else {
w.name("type").value("manual");
}
w.endObject();
} while (cursor.moveToNext());
w.endArray();
}
cursor.close();
}
public static ActivityEntity parseToActivity(JSONObject response, double unitMeters) throws JSONException {
ActivityEntity newActivity = new ActivityEntity();
newActivity.setSport(RunKeeperSynchronizer.runkeeper2sportMap.get(response.getString("type")).getDbValue());
if (response.has("notes")) {
newActivity.setComment(response.getString("notes"));
}
newActivity.setTime((long) Float.parseFloat(response.getString("duration")));
newActivity.setDistance(Float.parseFloat(response.getString("total_distance")));
String startTime = response.getString("start_time");
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US);
try {
newActivity.setStartTime(format.parse(startTime));
} catch (ParseException e) {
Log.e(Constants.LOG, e.getMessage());
return null;
}
List<LapEntity> laps = new ArrayList<LapEntity>();
List<LocationEntity> locations = new ArrayList<LocationEntity>();
JSONArray distance = response.getJSONArray("distance");
JSONArray path = response.getJSONArray("path");
JSONArray hr = response.getJSONArray("heart_rate");
SortedMap<Long, HashMap<String, String>> pointsValueMap = createPointsMap(distance, path, hr);
Iterator<Map.Entry<Long, HashMap<String, String>>> points = pointsValueMap.entrySet().iterator();
//lap hr
int maxHr = 0;
int sumHr = 0;
int count = 0;
//point speed
long time = 0;
float meters = 0.0f;
//activity hr
int maxHrOverall = 0;
int sumHrOverall = 0;
int countOverall = 0;
while (points.hasNext()) {
Map.Entry<Long, HashMap<String, String>> timePoint = points.next();
HashMap<String, String> values = timePoint.getValue();
LocationEntity lv = new LocationEntity();
lv.setActivityId(newActivity.getId());
lv.setTime(TimeUnit.SECONDS.toMillis(newActivity.getStartTime()) + timePoint.getKey());
String dist = values.get("distance");
if (dist == null) {
continue;
}
String lat = values.get("latitude");
String lon = values.get("longitude");
String alt = values.get("altitude");
String heart = values.get("heart_rate");
String type = values.get("type");
if (lat == null || lon == null) {
continue;
} else {
lv.setLatitude(Double.valueOf(lat));
lv.setLongitude(Double.valueOf(lon));
}
if (alt != null) {
lv.setAltitude(Double.valueOf(alt));
}
if (pointsValueMap.firstKey().equals(timePoint.getKey())) {
lv.setType(DB.LOCATION.TYPE_START);
} else if (!points.hasNext()) {
lv.setType(DB.LOCATION.TYPE_END);
} else if (type != null) {
lv.setType(RunKeeperSynchronizer.POINT_TYPE.get(type));
}
// lap and activity max and avg hr
if (heart != null) {
lv.setHr(Integer.valueOf(heart));
maxHr = Math.max(maxHr, lv.getHr());
maxHrOverall = Math.max(maxHrOverall, lv.getHr());
sumHr += lv.getHr();
sumHrOverall += lv.getHr();
count++;
countOverall++;
}
meters = Float.valueOf(dist) - meters;
time = timePoint.getKey() - time;
if (time > 0) {
float speed = meters / (float)TimeUnit.MILLISECONDS.toSeconds(time);
BigDecimal s = new BigDecimal(speed);
s = s.setScale(2, BigDecimal.ROUND_UP);
lv.setSpeed(s.floatValue());
}
// create lap if distance greater than configured lap distance
if (Float.valueOf(dist) >= unitMeters * laps.size()) {
LapEntity newLap = new LapEntity();
newLap.setLap(laps.size());
newLap.setDistance(Float.valueOf(dist));
newLap.setTime((int) TimeUnit.MILLISECONDS.toSeconds(timePoint.getKey()));
newLap.setActivityId(newActivity.getId());
laps.add(newLap);
// update previous lap with duration and distance
if (laps.size() > 1) {
LapEntity previousLap = laps.get(laps.size() - 2);
previousLap.setDistance(Float.valueOf(dist) - previousLap.getDistance());
previousLap.setTime((int) TimeUnit.MILLISECONDS.toSeconds(timePoint.getKey()) - previousLap.getTime());
if (hr != null && hr.length() > 0) {
previousLap.setMaxHr(maxHr);
previousLap.setAvgHr(sumHr / count);
}
maxHr = 0;
sumHr = 0;
count = 0;
}
}
// update last lap with duration and distance
if (!points.hasNext()) {
LapEntity previousLap = laps.get(laps.size() - 1);
previousLap.setDistance(Float.valueOf(dist) - previousLap.getDistance());
previousLap.setTime((int) TimeUnit.MILLISECONDS.toSeconds(timePoint.getKey()) - previousLap.getTime());
if (hr != null && hr.length() > 0) {
previousLap.setMaxHr(maxHr);
previousLap.setAvgHr(sumHr / count);
}
}
lv.setLap(laps.size()-1);
locations.add(lv);
}
// calculate avg and max hr
// update the activity
newActivity.setMaxHr(maxHrOverall);
if (countOverall > 0) {
newActivity.setAvgHr(sumHrOverall / countOverall);
}
newActivity.putPoints(locations);
newActivity.putLaps(laps);
return newActivity;
}
private static SortedMap<Long, HashMap<String, String>> createPointsMap(JSONArray distance, JSONArray path, JSONArray hr) throws JSONException {
SortedMap<Long, HashMap<String, String>> result = new TreeMap<Long, HashMap<String, String>>();
if (distance != null && distance.length() > 0) {
for (int i = 0; i < distance.length(); i++) {
JSONObject o = distance.getJSONObject(i);
Long key = TimeUnit.SECONDS.toMillis((long) Float.parseFloat(o.getString("timestamp")));
HashMap<String, String> value = new HashMap<String, String>();
String valueMapKey = "distance";
String valueMapValue = o.getString(valueMapKey);
value.put(valueMapKey, valueMapValue);
result.put(key, value);
}
}
if (path != null && path.length() > 0) {
for (int i = 0; i < path.length(); i++) {
JSONObject o = path.getJSONObject(i);
Long key = TimeUnit.SECONDS.toMillis((long)Float.parseFloat(o.getString("timestamp")));
HashMap<String, String> value = result.get(key);
if (value == null) {
value = new HashMap<String, String>();
}
String[] attrs = new String[] {"latitude", "longitude", "altitude", "type"};
for (String valueMapKey : attrs) {
String valueMapValue = o.getString(valueMapKey);
value.put(valueMapKey, valueMapValue);
}
result.put(key, value);
}
}
if (hr != null && hr.length() > 0) {
for (int i = 0; i < hr.length(); i++) {
JSONObject o = hr.getJSONObject(i);
Long key = TimeUnit.SECONDS.toMillis((long)Float.parseFloat(o.getString("timestamp")));
HashMap<String, String> value = result.get(key);
if (value == null) {
value = new HashMap<String, String>();
}
String valueMapKey = "heart_rate";
String valueMapValue = o.getString(valueMapKey);
value.put(valueMapKey, valueMapValue);
result.put(key, value);
}
}
return result;
}
}