package org.fluxtream.connectors.runkeeper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.fluxtream.core.connectors.Connector;
import org.fluxtream.core.connectors.ObjectType;
import org.fluxtream.core.domain.AbstractFacet;
import org.fluxtream.core.domain.ApiKey;
import org.fluxtream.core.domain.ChannelMapping;
import org.fluxtream.core.services.impl.BodyTrackHelper;
import org.fluxtream.core.services.impl.FieldHandler;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* User: candide
* Date: 21/10/13
* Time: 00:28
*/
@Component("runkeeperPace")
public class RunkeeperPaceFieldHandler implements FieldHandler {
@Autowired
BodyTrackHelper bodyTrackHelper;
public final static double MAX_MINUTES_PER_KM = 20.0;
@Override
public List<BodyTrackHelper.BodyTrackUploadResult> handleField (final ApiKey apiKey, AbstractFacet facet) {
RunKeeperFitnessActivityFacet activityFacet = (RunKeeperFitnessActivityFacet) facet;
if (activityFacet.distanceStorage == null) {
return Arrays.asList();
}
JSONArray distanceJson = JSONArray.fromObject(activityFacet.distanceStorage);
List<List<Object>> data = new ArrayList<List<Object>>();
double lastTimestamp = 0d;
double lastDistance = 0d;
for(int i=0; i<distanceJson.size(); i++) {
JSONObject record = distanceJson.getJSONObject(i);
final double totalDistance = record.getInt("distance");
final double timestamp = record.getInt("timestamp");
final double lap = timestamp - lastTimestamp;
final double distance = totalDistance - lastDistance;
lastTimestamp = timestamp;
lastDistance = totalDistance;
// Ignore datapoints where either the time delta or
// the distance is 0
if (distance==0||lap==0)
continue;
final double minutesPerKilometer = ((1000d/distance)*lap)/60d;
// Also ignore datapoints where the minutesPerKilometer
// is over a threshold. This happens when you've been stopped
// for a while during the time interval. If we include
// those datapoints then they dominate and can cause the
// real data we care about to be too small to see.
// We chose the limit of 20 min/km based on wikipedia's
// claims about human walking pace and on comparing
// unfiltered graphs against the graphs that runkeeper
// generates.
if(minutesPerKilometer > MAX_MINUTES_PER_KM)
continue;
// Set the time to be the center of the interval
// between the earlier and later of the readings
// used to generate this datapoint
double when = (((double)facet.start)/1000.0d) + timestamp - lap/2.0d;
List<Object> siRecord = new ArrayList<Object>();
siRecord.add(when);
siRecord.add(minutesPerKilometer);
siRecord.add(minutesPerKilometer/.621371192d);
data.add(siRecord);
}
final List<String> channelNames = Arrays.asList("minutesPerKilometer", "minutesPerMile");
// TODO: check the status code in the BodyTrackUploadResult
return Arrays.asList(bodyTrackHelper.uploadToBodyTrack(apiKey, "runkeeper", channelNames, data));
}
@Override
public void addToDeclaredChannelMappings(final ApiKey apiKey, final List<ChannelMapping> channelMappings) {
ChannelMapping minutesPerKilometerMapping = new ChannelMapping(
apiKey.getId(), apiKey.getGuestId(),
ChannelMapping.ChannelType.data,
ChannelMapping.TimeType.gmt,
ObjectType.getObjectType(apiKey.getConnector(), "fitnessActivity").value(),
apiKey.getConnector().getDeviceNickname(), "minutesPerKilometer",
apiKey.getConnector().getDeviceNickname(), "minutesPerKilometer");
ChannelMapping minutesPerMileMapping = new ChannelMapping(
apiKey.getId(), apiKey.getGuestId(),
ChannelMapping.ChannelType.data,
ChannelMapping.TimeType.gmt,
ObjectType.getObjectType(apiKey.getConnector(), "fitnessActivity").value(),
apiKey.getConnector().getDeviceNickname(), "minutesPerMile",
apiKey.getConnector().getDeviceNickname(), "minutesPerMile");
channelMappings.add(minutesPerKilometerMapping);
channelMappings.add(minutesPerMileMapping);
}
}