/*
* Copyright (C) 2016 rickyepoderi@yahoo.es
*
* 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.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.util.Log;
import org.runnerup.common.util.Constants;
import org.runnerup.workout.Sport;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/**
* Class that creates a POST data to submit to Runalyze using the activity information
* in the runnerup database. The class uses some constants that in Runalyze are
* inside the database (soports and types IDs, calories assigned to each sport and so on).
*/
public class RunalyzePost {
private SQLiteDatabase mDB = null;
private Map<String,Map<String,String>> sports;
private Map<String,Map<String,String>> types;
/**
* Constructor using the database.
* @param mDB The database to read the activity information
* @param sports The map of sports read in runalyze.
* @param types The map of types in runalyze.
*/
public RunalyzePost(SQLiteDatabase mDB, Map<String,Map<String,String>> sports, Map<String,Map<String,String>> types) {
this.mDB = mDB;
this.sports = sports;
this.types = types;
}
//
// FIELDS ABOUT LOCATION
//
/**
* Methos that creates the cursor over all the location track of the activity.
* @param activityId The activity id
* @return The cursor associated to the list of locations of the activity
*/
public Cursor createLocationCursor(long activityId) {
String[] columns = {
Constants.DB.LOCATION.TIME,
Constants.DB.LOCATION.LATITUDE,
Constants.DB.LOCATION.LONGITUDE,
Constants.DB.LOCATION.ALTITUDE,
Constants.DB.LOCATION.HR,
};
return mDB.query(Constants.DB.LOCATION.TABLE, columns,
Constants.DB.LOCATION.ACTIVITY + " = " + activityId, null, null, null, null);
}
/**
* Method that writes a value in POST format, the value is encoded
* properly. If it is null the defaultValue is used instead.
* @param value The value to write
* @param defaultValue The default value to use in case value is null
* @param writer The writer to write with
* @throws IOException Some error writing the value
*/
public void writeValue(String value, String defaultValue, Writer writer) throws IOException {
try {
if (value == null) {
value = defaultValue;
}
writer.write(URLEncoder.encode(value, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// it's impossible "UTF-8" is a valid encoding
}
}
/**
* This method writes the <em>arr_time</em> for runalyze. This field is an array of seconds
* since the beginning for each track point.
* @param c The cursor to iterate with the locations
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeArrTime(Cursor c, Writer writer) throws IOException {
writer.write("arr_time");
writer.write("=");
if (c.moveToFirst()) {
long start = c.getLong(0) / 1000;
writer.write(Long.toString(c.getLong(0) / 1000 - start));
while (c.moveToNext()) {
writer.write("|");
writer.write(Long.toString(c.getLong(0) / 1000 - start));
}
}
writer.write("&");
}
/**
* Method that writes the distance for each track point in kilometers (arr_dist).
* @param c The cursor to iterate with the locations
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeArrDist(Cursor c, Writer writer) throws IOException {
writer.write("arr_dist");
writer.write("=");
if (c.moveToFirst()) {
double lat = c.getDouble(1);
double lon = c.getDouble(2);
float totalDistance = 0;
writer.write(Float.toString(totalDistance));
while (c.moveToNext()) {
writer.write("|");
float d[] = {0};
double newLat = c.getDouble(1);
double newLon = c.getDouble(2);
Location.distanceBetween(lat, lon, newLat, newLon, d);
//Log.d(getClass().getName(), "Distance: [" + lat + ", " + lon + "] -> [" + newLat + ", " + newLon + "]");
totalDistance += d[0];
lat = newLat;
lon = newLon;
writer.write(Float.toString(totalDistance/1000.0F));
}
}
writer.write("&");
}
/**
* Generic method to write an array for runalyze with a direct column of the location cursor.
* @param name The name of the field in runalyze
* @param column The column position of the value
* @param defaultValue The default value if the column value is null
* @param c The cursor to iterate with the locations
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeLocationField(String name, int column, String defaultValue, Cursor c, Writer writer) throws IOException {
writer.write(name);
writer.write("=");
if (c.moveToFirst()) {
writeValue(c.getString(column), defaultValue, writer);
while (c.moveToNext()) {
writer.write("|");
writeValue(c.getString(column), defaultValue, writer);
}
}
writer.write("&");
}
/**
* Method that writes oll the post inputs that are arrays dependent of the location table.
* @param activityId The activity id to export
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeLocationFields(long activityId, Writer writer) throws IOException {
Cursor c = null;
try {
c = createLocationCursor(activityId);
writeArrTime(c, writer);
writeArrDist(c, writer);
writeLocationField("arr_lat", 1, null, c, writer);
writeLocationField("arr_lon", 2, null, c, writer);
writeLocationField("arr_alt", 3, "0", c, writer);
writeLocationField("arr_heart", 4, "0", c, writer);
} finally {
if (c != null) {
c.close();
}
}
}
//
// FIELDS ABOUT LAP
//
/**
* Methos that creates the cursor over all the laps of the activity.
* @param activityId The activity id
* @return The cursor associated to the list of laps of the activity
*/
public Cursor createLapCursor(long activityId) {
String[] columns = {
Constants.DB.LAP.DISTANCE,
Constants.DB.LAP.TIME,
//Constants.DB.LAP.INTENSITY,
};
return mDB.query(Constants.DB.LAP.TABLE, columns,
Constants.DB.LAP.ACTIVITY + " = " + activityId, null, null, null, null);
}
/**
* Method that write a typical post input <em>name=value</em> with the value properly encoded.
* @param name The name of the value
* @param value The value
* @param defaultValue The default value to use if the value is null
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeNomalField(String name, String value, String defaultValue, Writer writer) throws IOException {
writer.write(name);
writer.write("=");
writeValue(value, defaultValue, writer);
writer.write("&");
}
/**
* Method that transforms a time in seconds to runalyze TIME_MINUTES format (mm:ss).
* @param seconds The seconds to transform
* @return The string with the seconds in format mm:ss
*/
public String seconds2MinuteAndSeconds(long seconds) {
// runalyze has some data that is expressed in mm:ss (TIME_MINUTES)
// you can check it in class.FormularValueParser.php method validateTimeMinutes
long m = seconds / 60;
long s = seconds % 60;
return String.format(Locale.US, "%d:%02d", m, s);
}
/**
* Method that writes all runalyze fields that are dependant of the laps information.
* @param activityId The activity id to export
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeLapFields(long activityId, Writer writer) throws IOException {
Cursor c = null;
try {
c = createLapCursor(activityId);
boolean more = c.moveToFirst();
while (more) {
writeNomalField("splits[km][]", Float.toString(c.getFloat(0)/1000.0F), null, writer);
writeNomalField("splits[time][]", seconds2MinuteAndSeconds(c.getLong(1)), null, writer);
writeNomalField("splits[active][]", "1", null, writer);
more = c.moveToNext();
}
} finally {
if (c != null) {
c.close();
}
}
}
//
// FIELDS ABOUT ACTIVITY
//
/**
* Methos that creates the cursor over the activity itself (one row).
* @param activityId The activity id
* @return The cursor associated to the activity
*/
public Cursor createActivityCursor(long activityId) {
String[] columns = {
Constants.DB.ACTIVITY.START_TIME,
Constants.DB.ACTIVITY.TIME,
Constants.DB.ACTIVITY.DISTANCE,
Constants.DB.ACTIVITY.AVG_HR,
Constants.DB.ACTIVITY.MAX_HR,
Constants.DB.ACTIVITY.COMMENT,
Constants.DB.ACTIVITY.SPORT,
};
return mDB.query(Constants.DB.ACTIVITY.TABLE, columns, "_id = " + activityId,
null, null, null, null);
}
/**
* <em>Runalyze</em> calculates the calories using the table of sports. By default they
* are using this:
* <pre>
* | id | name | kcal |
* | 1 | Running | 880 |
* | 2 | Swimming | 743 |
* | 3 | Biking | 770 |
* | 4 | Gymnastics | 280 |
* | 5 | Other | 500 |
* </pre>
* @param dbValue The sport id used in runnerup
* @param seconds The time of the activity for the calculation
* @return The kCal used in string
*/
public String calculateKCal(int dbValue, long seconds) {
Sport sport = Sport.valueOf(dbValue);
if (sport.IsRunning()) {
return Integer.toString(Math.round((880.0F / 3600.0F) * seconds));
}
if (sport.IsCycling()) {
return Integer.toString(Math.round((770.0F / 3600.0F) * seconds));
}
//Constants.DB.ACTIVITY.SPORT_OTHER:
return Integer.toString(Math.round((500.0F / 3600.0F) * seconds));
}
/**
* Method that calculates sportid, typeid and kcal.
* Runalyze sport is read in the sports map. The exporter looks for a sport that contains the
* <em>runnerup</em> sport name ignoring case. If no sport is found a default "1" is returned.
* The <em>typeid</em> is just the lowest type if found.
* The <em>kcal</em> is calculated using the sport data if found, if not the calculateKCal default
* values is used.
*
* @param dbValue The value in the ddbb
* @param seconds The time of the activity for the calculation
* @throws IOException Some error
*/
public void calculateSportTypeAndKCal(int dbValue, long seconds, Writer writer) throws IOException {
//
// calculate sportid
Sport sport = Sport.valueOf(dbValue);
String sportName = sport.name().toLowerCase();
String sportId = "1"; // default values is 1 (just something)
String sportFound = null;
for (String runalyzeSport: sports.keySet()) {
if (runalyzeSport.toLowerCase().contains(sportName) &&
sports.get(runalyzeSport).containsKey("value")) {
sportId = sports.get(runalyzeSport).get("value");
sportFound = runalyzeSport;
}
}
Log.d(getClass().getName(), "Using runalyze sport=" + sportFound + "(" + sportId + ")");
writeNomalField("sportid", sportId, null, writer);
//
// calculate typeid
String typeFound = null;
String typeId = "";
if (sportFound != null) {
// the typeid will be the min value of that type
int typeIdInt = Integer.MAX_VALUE;
for (Map.Entry<String,Map<String,String>> e: types.entrySet()) {
if (sportId.equals(e.getValue().get("data-sport")) &&
e.getValue().containsKey("value") &&
typeIdInt > Integer.parseInt(e.getValue().get("value"))) {
typeIdInt = Integer.parseInt(e.getValue().get("value"));
typeFound = e.getKey();
}
}
if (typeIdInt != Integer.MAX_VALUE) {
typeId = Integer.toString(typeIdInt);
}
}
Log.d(getClass().getName(), "Using runalyze type=" + typeFound + "(" + typeId + ")");
writeNomalField("typeid", typeId, null, writer);
//
// calculate kcal
String kcal = null;
if (sportFound != null && sports.get(sportFound).containsKey("data-kcal")) {
kcal = Integer.toString(Math.round((Float.parseFloat(sports.get(sportFound).get("data-kcal")) / 3600.0F) * seconds));
Log.d(getClass().getName(), "Kcal using sports value: " + sports.get(sportFound).get("data-kcal"));
} else {
// calculate in default numbers
Log.d(getClass().getName(), "Kcal using defaults value");
kcal = calculateKCal(dbValue, seconds);
}
writeNomalField("kcal", kcal, null, writer);
}
/**
* Method that writes all the fields that are related to the activity information.
* @param activityId The activity id to export
* @param writer The writer to write
* @throws IOException Some error
*/
public void writeActivityFields(long activityId, Writer writer) throws IOException {
Cursor c = null;
try {
c = createActivityCursor(activityId);
if (c.moveToFirst()) {
writeNomalField("time_day", new SimpleDateFormat("dd.MM.yyyy", Locale.US).format(new Date(c.getLong(0) * 1000)), null, writer);
writeNomalField("time_daytime", new SimpleDateFormat("HH:mm", Locale.US).format(new Date(c.getLong(0) * 1000)), null, writer);
writeNomalField("s", seconds2MinuteAndSeconds(c.getLong(1)), null, writer);
writeNomalField("elapsed_time", c.getString(1), null, writer);
writeNomalField("distance", Float.toString(c.getFloat(2)/1000.0F), null, writer);
writeNomalField("pulse_avg", c.getString(3), "0", writer);
writeNomalField("pulse_max", c.getString(4), "0", writer);
writeNomalField("comment", c.getString(5), "", writer);
writeNomalField("pace", seconds2MinuteAndSeconds(Math.round(c.getFloat(1)*1000.0F/c.getFloat(2))), "", writer);
// write sportid, typeid and kcal depending the sports and types read from runalyze
calculateSportTypeAndKCal(c.getInt(6), c.getLong(1), writer);
// it seems that the activity_id in runalyze is set in "class.ParserAbstractSingle.php"
// and it is just the timestamp in seconds of the activity
writeNomalField("activity_id", Long.toString(c.getLong(0)), null, writer);
}
} finally {
if (c != null) {
c.close();
}
}
}
//
// FIELDS THAT ARE FIXED NOW
//
/**
* Method that writes the rest of information that is fixed.
* @param writer The writer to write.
* @throws IOException Some error
*/
public void writeFixedFields(Writer writer) throws IOException {
writeNomalField("creator", "", null, writer);
writeNomalField("creator_details", "", null, writer);
writeNomalField("timezone_offset", Integer.toString(TimeZone.getDefault().getRawOffset() / 60000), null, writer);
writeNomalField("arr_geohashes", "", null, writer);
writeNomalField("arr_alt_original", "", null, writer);
writeNomalField("arr_cadence", "", null, writer); // TODO: Maybe needed for biking???
writeNomalField("arr_power", "", null, writer);
writeNomalField("arr_temperature", "", null, writer);
writeNomalField("arr_groundcontact", "", null, writer);
writeNomalField("arr_vertical_oscillation", "", null, writer);
writeNomalField("arr_groundcontact_balance", "", null, writer);
writeNomalField("pauses", "[]", null, writer); // TODO: How to get the pauses?
writeNomalField("hrv", "", null, writer);
writeNomalField("fit_vdot_estimate", "0.00", null, writer);
writeNomalField("fit_recovery_time", "0", null, writer);
writeNomalField("fit_hrv_analysis", "0", null, writer);
writeNomalField("fit_training_effect", "", null, writer);
writeNomalField("fit_performance_condition", "", null, writer);
writeNomalField("elevation_calculated", "0", null, writer);
writeNomalField("groundcontact", "0", null, writer);
writeNomalField("vertical_oscillation", "0", null, writer);
writeNomalField("groundcontact_balance", "0", null, writer);
writeNomalField("vertical_ratio", "0", null, writer);
writeNomalField("stroke", "", null, writer);
writeNomalField("stroketype", "", null, writer);
writeNomalField("total_strokes", "0", null, writer);
writeNomalField("swolf", "0", null, writer);
writeNomalField("pool_length", "0", null, writer);
writeNomalField("weather_source", "", null, writer);
writeNomalField("is_night", "0", null, writer); // TODO: Calculate from position and time???
writeNomalField("distance-to-km-factor", "1", null, writer);
writeNomalField("is_race_sent", "true", null, writer);
writeNomalField("elevation", "0", null, writer);
writeNomalField("power", "0", null, writer);
writeNomalField("cadence", "0", null, writer);
writeNomalField("use_vdot", "on", null, writer);
writeNomalField("rpe", "", null, writer);
writeNomalField("partner", "", null, writer);
writeNomalField("route", "", null, writer);
writeNomalField("notes", "", null, writer); // TODO: Use notes or comment???
writeNomalField("weatherid", "1", null, writer);
writeNomalField("temperature", "", null, writer);
writeNomalField("wind_speed", "", null, writer);
writeNomalField("wind_deg", "", null, writer);
writeNomalField("humidity", "", null, writer);
writeNomalField("pressure", "", null, writer);
writeNomalField("tag_old", "", null, writer);
writeNomalField("equipment_old", "", null, writer);
writeNomalField("submit", "submit", null, writer);
}
/**
* Method that construct the complete post information using the database.
* @param activityId The activity id to export
* @param writer Thw writer to write
* @throws IOException Some error
*/
public void export(long activityId, Writer writer) throws IOException {
writeActivityFields(activityId, writer);
writeLapFields(activityId, writer);
writeLocationFields(activityId, writer);
writeFixedFields(writer);
}
}