/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.android.apps.mytracks.util; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint.WaypointType; import com.google.android.apps.mytracks.stats.TripStatisticsUpdater; import com.google.android.maps.mytracks.R; import android.content.Context; import android.database.Cursor; import android.location.Location; /** * Utilities to calculate calories. * * @author youtaol */ public class CalorieUtils { /** * Activity types. */ public enum ActivityType { CYCLING, RUNNING, WALKING, INVALID } private CalorieUtils() {} /** * Resting VO2 is 3.5 milliliters per kilogram of body weight per minute * (ml/kg/min). The resting VO2 is the same for everyone. */ private static final double RESTING_VO2 = 3.5; /** * Standard gravity in meters per second squared (m/s^2). */ private static final double EARTH_GRAVITY = 9.81; /** * Lumped constant for all frictional losses (tires, bearings, chain). */ private static final double K1 = 0.0053; /** * Lumped constant for aerodynamic drag (kg/m) */ private static final double K2 = 0.185; /** * Gets the activity type. * * @param context the context * @param activityType the activity type */ public static ActivityType getActivityType(Context context, String activityType) { if (activityType == null || activityType.equals("")) { return ActivityType.INVALID; } if (TrackIconUtils.getIconValue(context, activityType).equals(TrackIconUtils.WALK)) { return ActivityType.WALKING; } else if (TrackIconUtils.getIconValue(context, activityType).equals(TrackIconUtils.RUN)) { return ActivityType.RUNNING; } else if (TrackIconUtils.getIconValue(context, activityType).equals(TrackIconUtils.BIKE)) { return ActivityType.CYCLING; } return ActivityType.INVALID; } /** * Updates calories for a track and its waypoints. * * @param context the context * @param track the track * @return an array of two doubles, first is the track calorie, second is the * last statistics waypoint calorie */ public static double[] updateTrackCalorie(Context context, Track track) { MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); ActivityType activityType = getActivityType(context, track.getCategory()); if (activityType == ActivityType.INVALID) { clearCalorie(myTracksProviderUtils, track); return new double[] { 0.0, 0.0 }; } TripStatisticsUpdater trackTripStatisticsUpdater = new TripStatisticsUpdater( track.getTripStatistics().getStartTime()); TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater( track.getTripStatistics().getStartTime()); int recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT); double weight = PreferencesUtils.getFloat( context, R.string.weight_key, PreferencesUtils.getDefaultWeight(context)); LocationIterator locationIterator = null; Cursor cursor = null; try { Waypoint waypoint = null; locationIterator = myTracksProviderUtils.getTrackPointLocationIterator( track.getId(), -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1); if (cursor != null && cursor.moveToFirst()) { /* * Yes, this will skip the first waypoint and that is intentional as the * first waypoint holds the stats for the track. */ waypoint = getNextStatisticsWaypoint(myTracksProviderUtils, cursor); } while (locationIterator.hasNext()) { Location location = locationIterator.next(); trackTripStatisticsUpdater.addLocation( location, recordingDistanceInterval, true, activityType, weight); markerTripStatisticsUpdater.addLocation( location, recordingDistanceInterval, true, activityType, weight); if (waypoint != null && waypoint.getLocation().getTime() == location.getTime() && waypoint.getLocation().getLatitude() == location.getLatitude() && waypoint.getLocation().getLongitude() == location.getLongitude()) { waypoint.getTripStatistics() .setCalorie(markerTripStatisticsUpdater.getTripStatistics().getCalorie()); myTracksProviderUtils.updateWaypoint(waypoint); markerTripStatisticsUpdater = new TripStatisticsUpdater(location.getTime()); waypoint = getNextStatisticsWaypoint(myTracksProviderUtils, cursor); } } } finally { if (locationIterator != null) { locationIterator.close(); } if (cursor != null) { cursor.close(); } } double trackCalorie = trackTripStatisticsUpdater.getTripStatistics().getCalorie(); track.getTripStatistics().setCalorie(trackCalorie); myTracksProviderUtils.updateTrack(track); return new double[] { trackCalorie, markerTripStatisticsUpdater.getTripStatistics().getCalorie() }; } /** * Gets the calorie in kcal between two locations. * * @param start the start location * @param stop the stop location * @param grade the grade * @param weight the weight in kilogram. For cycling, weight of the rider plus * bike. For foot, weight of the user * @param activityType the activity type */ public static double getCalorie( Location start, Location stop, double grade, double weight, ActivityType activityType) { if (activityType == ActivityType.INVALID) { return 0.0; } if (grade < 0) { grade = 0.0; } // Speed in m/s double speed = (start.getSpeed() + stop.getSpeed()) / 2.0; // Duration in min double duration = (double) (stop.getTime() - start.getTime()) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN; if (activityType == ActivityType.CYCLING) { /* * Power in watt (Joule/second). See * http://en.wikipedia.org/wiki/Bicycle_performance. */ double power = EARTH_GRAVITY * weight * speed * (K1 + grade) + K2 * (speed * speed * speed); // WorkRate in kgm/min double workRate = power * UnitConversions.W_TO_KGM; /* * VO2 in kgm/min/kg 1.8 = oxygen cost of producing 1 kgm/min of power * output. 7 = oxygen cost of unloaded cycling plus resting oxygen * consumption */ double vo2 = (1.8 * workRate / weight) + 7; // Calorie in kcal return vo2 * duration * weight * UnitConversions.KGM_TO_KCAL; } else { double vo2 = activityType == ActivityType.RUNNING ? getRunningVo2(speed, grade) : getWalkingVo2(speed, grade); /* * Calorie in kcal (mL/kg/min * min * kg * L/mL * kcal/L) */ return vo2 * duration * weight * UnitConversions.ML_TO_L * UnitConversions.L_TO_KCAL; } } /** * Clears calorie in the track and its waypoints. * * @param myTracksProviderUtils the my tracks provider utils * @param track the track */ private static void clearCalorie(MyTracksProviderUtils myTracksProviderUtils, Track track) { track.getTripStatistics().setCalorie(0); myTracksProviderUtils.updateTrack(track); Cursor cursor = null; try { cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1); if (cursor != null && cursor.moveToFirst()) { /* * Yes, this will skip the first waypoint and that is intentional as the * first waypoint holds the stats for the track. */ Waypoint waypoint = getNextStatisticsWaypoint(myTracksProviderUtils, cursor); while (waypoint != null) { waypoint.getTripStatistics().setCalorie(0); myTracksProviderUtils.updateWaypoint(waypoint); waypoint = getNextStatisticsWaypoint(myTracksProviderUtils, cursor); } } } finally { if (cursor != null) { cursor.close(); } } } /** * Gets the next statistics waypoint from a cursor. * * @param myTracksProviderUtils the my tracks provider utils * @param cursor the cursor */ private static Waypoint getNextStatisticsWaypoint( MyTracksProviderUtils myTracksProviderUtils, Cursor cursor) { if (cursor == null) { return null; } while (cursor.moveToNext()) { Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor); if (waypoint.getType() == WaypointType.STATISTICS) { return waypoint; } } return null; } /** * Gets the running VO2 in ml/kg/min. This equation is appropriate for speeds * greater than 5 mi/hr (or 3 mi/hr or greater if the subject is truly * jogging). * * @param speed the speed in m/s * @param grade the grade */ private static double getRunningVo2(double speed, double grade) { // Change from m/s to m/min speed = speed / UnitConversions.S_TO_MIN; /* * 0.2 = oxygen cost per meter of moving each kg of body weight while * running (horizontally). 0.9 = oxygen cost per meter of moving total body * mass against gravity (vertically). */ return 0.2 * speed + 0.9 * speed * grade + RESTING_VO2; } /** * Gets the walking VO2 in ml/kg/min. This equation is appropriate for speed * from 1.9 to 4 mi/hr. * * @param speed the speed in m/s * @param grade the grade */ private static double getWalkingVo2(double speed, double grade) { // Change from m/s to m/min speed = speed / UnitConversions.S_TO_MIN; /* * 0.1 = oxygen cost per meter of moving each kilogram (kg) of body weight * while walking (horizontally). 1.8 = oxygen cost per meter of moving total * body mass against gravity (vertically). */ return 0.1 * speed + 1.8 * speed * grade + RESTING_VO2; } }