/**
* ORcycle, Copyright 2014, 2015, PSU Transportation, Technology, and People Lab.
*
* @author Robin Murray <robin5@pdx.edu> (code)
* @author Miguel Figliozzi <figliozzi@pdx.edu> and ORcycle team (general app
* design and features, report questionnaires and new ORcycle features)
*
* For more information on the project, go to
* http://www.pdx.edu/transportation-lab/orcycle and http://www.pdx.edu/transportation-lab/app-development
*
* Updated/modified for Oregon pilot study and app deployment.
*
* ORcycle 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 any later version.
* ORcycle 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
* ORcycle. If not, see <http://www.gnu.org/licenses/>.
*
*/
package edu.pdx.cecs.orcycle;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Handler;
import android.util.Log;
public class SpeedMonitor {
private static final String MODULE_TAG = "SpeedMonitor";
private static final long ONE_MINUTE_MS = 60000;
private static final long TWO_MINUTES_MS = 120000;
private static final long THREE_MINUTES_MS = 180000; // For faster debugging, set this value to 18000
private static final float METERS_PER_SECOND_TO_MILES_PER_HOUR = 2.2369f;
private static final String MESSAGE_TOO_SLOW = "You are going slower than 3 mph, if you are not biking anymore, please stop recording the trip. Thanks!";
private static final String MESSAGE_TOO_FAST = "You are going faster than 20 mph, if you are not biking anymore, please stop recording the trip. Thanks!";
private final LinkedList<ListItem> speeds = new LinkedList<ListItem>();
private final SoundPool soundpool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
private final Handler speedCheckHandler = new Handler();
private final Context context;
private final int bikebell;
private Timer speedCheckTimer;
// *********************************************************************************
// *
// *********************************************************************************
/**
* TimerTask for calculating the average speed
*/
private final class CalculateAverageSpeedTask extends TimerTask {
@Override
public void run() {
try {
if (null == speedCheckHandler) {
Log.e(MODULE_TAG, "speedCheckHandler == null");
}
else {
speedCheckHandler.post(new Runnable() {
public void run() {
try {
speedCheck();
}
catch(Exception ex) {
Log.e(MODULE_TAG, ex.getMessage());
}
}
});
}
}
catch(Exception ex) {
Log.e(MODULE_TAG, ex.getMessage());
}
}
}
/**
* Item for storing an instance of time and speed
*/
private final class ListItem {
long time;
float speed;
public ListItem(long time, float speed) {
this.time = time;
this.speed = speed;
}
}
// *********************************************************************************
// *
// *********************************************************************************
/**
* Constructor
* @param context
*/
public SpeedMonitor(Context context) {
this.context = context;
this.bikebell = soundpool.load(context, R.raw.bikebell, 1);
}
/**
* Start monitoring of speed
*/
public void start() {
try {
cancel();
speeds.clear();
startTimer();
}
catch(Exception ex) {
Log.e(MODULE_TAG, ex.getMessage());
}
}
/**
* Stop monitoring of speed
*/
public void cancel() {
if (null != speedCheckTimer) {
speedCheckTimer.cancel();
speedCheckTimer = null;
}
}
/**
* Stores a time and speed measurement
* @param currentTimeMillis
* @param speed
*/
public void recordSpeed(long currentTimeMillis, float speed) {
speeds.addFirst(new ListItem(currentTimeMillis, speed));
}
// *********************************************************************************
// *
// *********************************************************************************
/**
* Starts timer for checking speed list
*/
private void startTimer() {
speedCheckTimer = new Timer();
speedCheckTimer.schedule(new CalculateAverageSpeedTask(), THREE_MINUTES_MS, ONE_MINUTE_MS);
}
/**
* Checks speed running average and if outside of range, alerts the user
*/
private void speedCheck() {
float [] runningAverage = new float[1];
if (getAverage(THREE_MINUTES_MS, runningAverage)) {
float mph = runningAverage[0] * METERS_PER_SECOND_TO_MILES_PER_HOUR;
// Note: if we get here then there is 3 minutes
// or more data in speed buffer
if (mph < 3.0f) {
notifyUser(MESSAGE_TOO_SLOW);
truncateBufferTo(0);
}
else if (mph > 20.0f) {
notifyUser(MESSAGE_TOO_FAST);
truncateBufferTo(0);
}
else {
// The timer will schedule the next check in a minute, so
// remove all but last two minutes of data from buffer
truncateBufferTo(TWO_MINUTES_MS);
}
}
}
/**
* Get the average speed over the specified time period ()
* @param timePeriod time to take average over (specified in milliseconds)
* @param runningAverage returns average running speed (expressed in meters/second)
* @return true if calculation made, false otherwise
*/
private boolean getAverage(long timePeriod, float [] runningAverage) {
try {
long currentTime = System.currentTimeMillis();
// Determine if buffer contains at least 3 minutes of buffered speed data
if(speeds.size() == 0) {
return false;
}
else if ((currentTime - speeds.getLast().time) < timePeriod) {
return false;
}
// Calculate average speed
Iterator<ListItem> entries = speeds.iterator();
ListItem entry;
float speedSummation = 0.0f;
while (entries.hasNext()) {
entry = entries.next();
speedSummation += entry.speed;
}
runningAverage[0] = speedSummation / speeds.size();
return true;
}
catch(Exception ex) {
Log.v(MODULE_TAG, ex.getMessage());
}
return false;
}
/**
* Removes entries from list if recorded more than timePeriod
* ago. If timePeriod <= 0, all entries are removed.
* @param timePeriod
*/
private void truncateBufferTo(long timePeriod) {
try {
if (timePeriod > 0) {
// Get current time
long currentTime = System.currentTimeMillis();
// Iterate over speeds
Iterator<ListItem> entries = speeds.iterator();
while (entries.hasNext()) {
// Remove speed if recorded more than timePeriod ago
if ((currentTime - entries.next().time) > timePeriod) {
entries.remove();
}
}
}
else {
speeds.clear();
}
}
catch(Exception ex) {
Log.e(MODULE_TAG, ex.getMessage());
}
}
/**
* Plays bell sound and presents message to user via toast
* @param message
*/
private void notifyUser(String message) {
try {
soundpool.play(bikebell, 1.0f, 1.0f, 1, 0, 1.0f);
CustomToast toast = new CustomToast(context, message, 3);
toast.show();
}
catch(Exception ex) {
Log.v(MODULE_TAG, ex.getMessage());
}
}
}