Clean up classes in the stats package

- TripStatisticsBuilder
- TripStatistics
- ExtremityMonitor
- DoubleBuffer
This commit is contained in:
Jimmy Shih
2012-08-08 10:17:32 -07:00
parent a0b3e21b14
commit 40e2aba739
9 changed files with 680 additions and 808 deletions
@@ -306,7 +306,7 @@ public class GpxImporter extends DefaultHandler {
tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder = new TripStatisticsBuilder(0);
tripStatisticsBuilder.pauseAt(0); tripStatisticsBuilder.pauseAt(0);
} }
track.setTripStatistics(tripStatisticsBuilder.getStatistics()); track.setTripStatistics(tripStatisticsBuilder.getTripStatistics());
track.setNumberOfPoints(numberOfLocations); track.setNumberOfPoints(numberOfLocations);
myTracksProviderUtils.updateTrack(track); myTracksProviderUtils.updateTrack(track);
tracksIds.add(track.getId()); tracksIds.add(track.getId());
@@ -253,7 +253,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
elevationBuffer.setNext(metricUnits ? location.getAltitude() elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT); : location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) { if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
} }
lastLocation = location; lastLocation = location;
@@ -279,7 +279,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
// Create an end marker // Create an end marker
if (lastLocation != null) { if (lastLocation != null) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context); DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription( track.setDescription(
@@ -297,7 +297,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
elevationBuffer.setNext(metricUnits ? location.getAltitude() elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT); : location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) { if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
} }
lastLocation = location; lastLocation = location;
@@ -323,7 +323,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
// Create an end marker // Create an end marker
if (lastLocation != null) { if (lastLocation != null) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
track.setDescription(getTrackDescription(track, distances, elevations)); track.setDescription(getTrackDescription(track, distances, elevations));
@@ -654,7 +654,7 @@ public class TrackRecordingService extends Service {
} }
} while (cursor.moveToPrevious()); } while (cursor.moveToPrevious());
} }
statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); statsBuilder.getTripStatistics().setMovingTime(stats.getMovingTime());
statsBuilder.pauseAt(stats.getStopTime()); statsBuilder.pauseAt(stats.getStopTime());
statsBuilder.resumeAt(System.currentTimeMillis()); statsBuilder.resumeAt(System.currentTimeMillis());
} else { } else {
@@ -834,7 +834,7 @@ public class TrackRecordingService extends Service {
// Update the current track: // Update the current track:
if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90) { if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90) {
TripStatistics tripStatistics = statsBuilder.getStatistics(); TripStatistics tripStatistics = statsBuilder.getTripStatistics();
tripStatistics.setStopTime(System.currentTimeMillis()); tripStatistics.setStopTime(System.currentTimeMillis());
if (recordingTrack.getStartId() < 0) { if (recordingTrack.getStartId() < 0) {
@@ -865,8 +865,8 @@ public class TrackRecordingService extends Service {
if (waypoint != null) { if (waypoint != null) {
waypoint.setLength(length); waypoint.setLength(length);
waypoint.setDuration( waypoint.setDuration(
System.currentTimeMillis() - statsBuilder.getStatistics().getStartTime()); System.currentTimeMillis() - statsBuilder.getTripStatistics().getStartTime());
waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); waypoint.setTripStatistics(waypointStatsBuilder.getTripStatistics());
providerUtils.updateWaypoint(waypoint); providerUtils.updateWaypoint(waypoint);
} }
} }
@@ -896,7 +896,7 @@ public class TrackRecordingService extends Service {
} }
waypoint.setTrackId(recordingTrackId); waypoint.setTrackId(recordingTrackId);
waypoint.setLength(length); waypoint.setLength(length);
if (lastLocation == null || statsBuilder == null || statsBuilder.getStatistics() == null) { if (lastLocation == null || statsBuilder == null || statsBuilder.getTripStatistics() == null) {
if (!request.isTrackStatistics()) { if (!request.isTrackStatistics()) {
return -1L; return -1L;
} }
@@ -910,7 +910,7 @@ public class TrackRecordingService extends Service {
waypoint.setLocation(location); waypoint.setLocation(location);
} else { } else {
waypoint.setLocation(lastLocation); waypoint.setLocation(lastLocation);
waypoint.setDuration(lastLocation.getTime() - statsBuilder.getStatistics().getStartTime()); waypoint.setDuration(lastLocation.getTime() - statsBuilder.getTripStatistics().getStartTime());
} }
Uri uri = providerUtils.insertWaypoint(waypoint); Uri uri = providerUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment()); return Long.parseLong(uri.getLastPathSegment());
@@ -958,7 +958,7 @@ public class TrackRecordingService extends Service {
// Override the duration - it's not the duration from the last waypoint, but // Override the duration - it's not the duration from the last waypoint, but
// the duration from the beginning of the whole track // the duration from the beginning of the whole track
waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime()); waypoint.setDuration(time - statsBuilder.getTripStatistics().getStartTime());
// Set the rest of the waypoint data // Set the rest of the waypoint data
waypoint.setType(Waypoint.TYPE_STATISTICS); waypoint.setType(Waypoint.TYPE_STATISTICS);
@@ -973,7 +973,7 @@ public class TrackRecordingService extends Service {
name = getString(R.string.marker_split_name_format, nextMarkerNumber); name = getString(R.string.marker_split_name_format, nextMarkerNumber);
} }
waypoint.setName(name); waypoint.setName(name);
waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); waypoint.setTripStatistics(waypointStatsBuilder.getTripStatistics());
waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint)); waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint));
waypoint.setIcon(getString(R.string.marker_statistics_icon_url)); waypoint.setIcon(getString(R.string.marker_statistics_icon_url));
@@ -1050,7 +1050,7 @@ public class TrackRecordingService extends Service {
} }
public TripStatistics getTripStatistics() { public TripStatistics getTripStatistics() {
return statsBuilder.getStatistics(); return statsBuilder.getTripStatistics();
} }
Location getLastLocation() { Location getLastLocation() {
@@ -25,61 +25,30 @@ package com.google.android.apps.mytracks.stats;
*/ */
public class DoubleBuffer { public class DoubleBuffer {
/** // The location that the next write will occur at.
* The location that the next write will occur at.
*/
private int index; private int index;
/** // The sliding buffer of doubles.
* The sliding buffer of doubles.
*/
private final double[] buffer; private final double[] buffer;
/** // True if the buffer is full
* Have all of the slots in the buffer been filled?
*/
private boolean isFull; private boolean isFull;
/** /**
* Creates a buffer with size elements. * Creates a buffer with a certain size.
* *
* @param size the number of elements in the buffer * @param size the size
* @throws IllegalArgumentException if the size is not a positive value
*/ */
public DoubleBuffer(int size) { public DoubleBuffer(int size) {
if (size < 1) { if (size < 1) {
throw new IllegalArgumentException("The buffer size must be positive."); throw new IllegalArgumentException("The buffer size must be greater than 1.");
} }
buffer = new double[size]; buffer = new double[size];
reset(); reset();
} }
/** /**
* Adds a double to the buffer. If the buffer is full the oldest element is * Resets the buffer.
* overwritten.
*
* @param d the double to add
*/
public void setNext(double d) {
if (index == buffer.length) {
index = 0;
}
buffer[index] = d;
index++;
if (index == buffer.length) {
isFull = true;
}
}
/**
* Are all of the entries in the buffer used?
*/
public boolean isFull() {
return isFull;
}
/**
* Resets the buffer to the initial state.
*/ */
public void reset() { public void reset() {
index = 0; index = 0;
@@ -87,16 +56,20 @@ public class DoubleBuffer {
} }
/** /**
* Gets the average of values from the buffer. * Returns true if the buffer is full.
* */
* @return The average of the buffer public boolean isFull() {
return isFull;
}
/**
* Gets the average of the buffer.
*/ */
public double getAverage() { public double getAverage() {
int numberOfEntries = isFull ? buffer.length : index; int numberOfEntries = isFull ? buffer.length : index;
if (numberOfEntries == 0) { if (numberOfEntries == 0) {
return 0; return 0;
} }
double sum = 0; double sum = 0;
for (int i = 0; i < numberOfEntries; i++) { for (int i = 0; i < numberOfEntries; i++) {
sum += buffer[i]; sum += buffer[i];
@@ -105,15 +78,15 @@ public class DoubleBuffer {
} }
/** /**
* Gets the average and standard deviation of the buffer. * Gets the average and the variance of the buffer.
* *
* @return An array of two elements - the first is the average, and the second * @return an array of two elements - the first is the average, the second is
* is the variance * the variance
*/ */
public double[] getAverageAndVariance() { public double[] getAverageAndVariance() {
int numberOfEntries = isFull ? buffer.length : index; int numberOfEntries = isFull ? buffer.length : index;
if (numberOfEntries == 0) { if (numberOfEntries == 0) {
return new double[]{0, 0}; return new double[] { 0, 0 };
} }
double sum = 0; double sum = 0;
@@ -124,8 +97,25 @@ public class DoubleBuffer {
} }
double average = sum / numberOfEntries; double average = sum / numberOfEntries;
return new double[]{average, double variance = sumSquares / numberOfEntries - Math.pow(average, 2);
sumSquares / numberOfEntries - Math.pow(average, 2)}; return new double[] { average, variance };
}
/**
* Adds a double to the buffer. If the buffer is full the oldest element is
* overwritten.
*
* @param value the double to add
*/
public void setNext(double value) {
if (index == buffer.length) {
index = 0;
}
buffer[index] = value;
index++;
if (index == buffer.length) {
isFull = true;
}
} }
@Override @Override
@@ -20,352 +20,307 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
/** /**
* Statistics keeper for a trip. * Builder for {@link TripStatistics}. For keeping statistics as a track is
* paused/resumed and new locations are added.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TripStatisticsBuilder { public class TripStatisticsBuilder {
/**
* Statistical data about the trip, which can be displayed to the user.
*/
private final TripStatistics data;
/** // The trip statistics.
* The last location that the gps reported. private final TripStatistics tripStatistics;
*/
private Location lastLocation;
/**
* The last location that contributed to the stats. It is also the last
* location the user was found to be moving.
*/
private Location lastMovingLocation;
/**
* The current speed in meters/second as reported by the gps.
*/
private double currentSpeed;
/**
* The current grade. This value is very noisy and not reported to the user.
*/
private double currentGrade;
/**
* Is the trip currently paused?
* All trips start paused.
*/
private boolean paused = true;
/**
* A buffer of the last speed readings in meters/second.
*/
private final DoubleBuffer speedBuffer =
new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
/**
* A buffer of the recent elevation readings in meters.
*/
private final DoubleBuffer elevationBuffer =
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
/**
* A buffer of the distance between recent gps readings in meters.
*/
private final DoubleBuffer distanceBuffer =
new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR);
/**
* A buffer of the recent grade calculations.
*/
private final DoubleBuffer gradeBuffer =
new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR);
/**
* The total number of locations in this trip.
*/
private long totalLocations = 0;
// The minimum recording distance.
private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT; private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT;
// True if the trip is paused. All trips start as paused.
private boolean paused = true;
// The last location as reported by GPS.
private Location lastLocation;
// The last moving location that contributed to the moving statistics.
private Location lastMovingLocation;
// A buffer of the recent speed readings (m/s) for calculating max speed.
private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
// A buffer of the recent elevation readings (m).
private final DoubleBuffer elevationBuffer = new DoubleBuffer(
Constants.ELEVATION_SMOOTHING_FACTOR);
// A buffer of the recent distance readings for calculating grade.
private final DoubleBuffer distanceBuffer = new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR);
// A buffer of the recent grade calculations
private final DoubleBuffer gradeBuffer = new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR);
/** /**
* Creates a new trip starting at the given time. * Creates a new trip starting at a start time.
* *
* @param startTime the start time. * @param startTime the start time
*/ */
public TripStatisticsBuilder(long startTime) { public TripStatisticsBuilder(long startTime) {
data = new TripStatistics(); tripStatistics = new TripStatistics();
resumeAt(startTime); resumeAt(startTime);
} }
/** /**
* Creates a new trip, starting with existing statistics data. * Creates a new trip, starting with an existing {@link TripStatistics}.
* *
* @param statsData the statistics data to copy and start from * @param other the existing {@link TripStatistics}
*/ */
public TripStatisticsBuilder(TripStatistics statsData) { public TripStatisticsBuilder(TripStatistics other) {
data = new TripStatistics(statsData); tripStatistics = new TripStatistics(other);
if (data.getStartTime() > 0) { if (tripStatistics.getStartTime() > 0) {
resumeAt(data.getStartTime()); resumeAt(tripStatistics.getStartTime());
} }
} }
/** /**
* Adds a location to the current trip. This will update all of the internal * Sets the min recording distance.
* variables with this new location.
* *
* @param currentLocation the current gps location * @param minRecordingDistance the min recording distance
* @param systemTime the time used for calculation of totalTime. This should
* be the phone's time (not GPS time)
* @return true if the person is moving
*/ */
public boolean addLocation(Location currentLocation, long systemTime) { public void setMinRecordingDistance(int minRecordingDistance) {
if (paused) { this.minRecordingDistance = minRecordingDistance;
Log.w(TAG,
"Tried to account for location while track is paused");
return false;
}
totalLocations++;
double elevationDifference = updateElevation(currentLocation.getAltitude());
// Update the "instant" values:
data.setTotalTime(systemTime - data.getStartTime());
currentSpeed = currentLocation.getSpeed();
// This was the 1st location added, remember it and do nothing else:
if (lastLocation == null) {
lastLocation = currentLocation;
lastMovingLocation = currentLocation;
return false;
}
updateBounds(currentLocation);
// Don't do anything if we didn't move since last fix:
double distance = lastLocation.distanceTo(currentLocation);
if (distance < minRecordingDistance &&
currentSpeed < Constants.MAX_NO_MOVEMENT_SPEED) {
lastLocation = currentLocation;
return false;
}
data.addTotalDistance(lastMovingLocation.distanceTo(currentLocation));
updateSpeed(currentLocation.getTime(), currentSpeed,
lastLocation.getTime(), lastLocation.getSpeed());
updateGrade(distance, elevationDifference);
lastLocation = currentLocation;
lastMovingLocation = currentLocation;
return true;
} }
/** /**
* Updates the track's bounding box to include the given location. * Resumes the current track at a given time.
*/
private void updateBounds(Location location) {
data.updateLatitudeExtremities(location.getLatitude());
data.updateLongitudeExtremities(location.getLongitude());
}
/**
* Updates the elevation measurements.
* *
* @param elevation the current elevation * @param time the time
*/ */
// @VisibleForTesting public void resumeAt(long time) {
double updateElevation(double elevation) { if (!paused) {
double oldSmoothedElevation = getSmoothedElevation();
elevationBuffer.setNext(elevation);
double smoothedElevation = getSmoothedElevation();
data.updateElevationExtremities(smoothedElevation);
double elevationDifference = elevationBuffer.isFull()
? smoothedElevation - oldSmoothedElevation
: 0.0;
if (elevationDifference > 0) {
data.addTotalElevationGain(elevationDifference);
}
return elevationDifference;
}
/**
* Updates the speed measurements.
*
* @param updateTime the time of the speed update
* @param speed the current speed
* @param lastLocationTime the time of the last speed update
* @param lastLocationSpeed the speed of the last update
*/
// @VisibleForTesting
void updateSpeed(long updateTime, double speed, long lastLocationTime,
double lastLocationSpeed) {
// We are now sure the user is moving.
long timeDifference = updateTime - lastLocationTime;
if (timeDifference < 0) {
Log.e(TAG,
"Found negative time change: " + timeDifference);
}
data.addMovingTime(timeDifference);
if (isValidSpeed(updateTime, speed, lastLocationTime, lastLocationSpeed,
speedBuffer)) {
speedBuffer.setNext(speed);
if (speed > data.getMaxSpeed()) {
data.setMaxSpeed(speed);
}
double movingSpeed = data.getAverageMovingSpeed();
if (speedBuffer.isFull() && (movingSpeed > data.getMaxSpeed())) {
data.setMaxSpeed(movingSpeed);
}
} else {
Log.d(TAG,
"TripStatistics ignoring big change: Raw Speed: " + speed
+ " old: " + lastLocationSpeed + " [" + toString() + "]");
}
}
/**
* Checks to see if this is a valid speed.
*
* @param updateTime The time at the current reading
* @param speed The current speed
* @param lastLocationTime The time at the last location
* @param lastLocationSpeed Speed at the last location
* @param speedBuffer A buffer of recent readings
* @return True if this is likely a valid speed
*/
public static boolean isValidSpeed(long updateTime, double speed,
long lastLocationTime, double lastLocationSpeed,
DoubleBuffer speedBuffer) {
// We don't want to count 0 towards the speed.
if (speed == 0) {
return false;
}
// We are now sure the user is moving.
long timeDifference = updateTime - lastLocationTime;
// There are a lot of noisy speed readings.
// Do the cheapest checks first, most expensive last.
// The following code will ignore unlikely to be real readings.
// - 128 m/s seems to be an internal android error code.
if (Math.abs(speed - 128) < 1) {
return false;
}
// Another check for a spurious reading. See if the path seems physically
// likely. Ignore any speeds that imply accelaration greater than 2g's
// Really who can accelerate faster?
double speedDifference = Math.abs(lastLocationSpeed - speed);
if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) {
return false;
}
// There are three additional checks if the reading gets this far:
// - Only use the speed if the buffer is full
// - Check that the current speed is less than 10x the recent smoothed speed
// - Double check that the current speed does not imply crazy acceleration
double smoothedSpeed = speedBuffer.getAverage();
double smoothedDiff = Math.abs(smoothedSpeed - speed);
return !speedBuffer.isFull() ||
(speed < smoothedSpeed * 10
&& smoothedDiff < Constants.MAX_ACCELERATION * timeDifference);
}
/**
* Updates the grade measurements.
*
* @param distance the distance the user just traveled
* @param elevationDifference the elevation difference between the current
* reading and the previous reading
*/
// @VisibleForTesting
void updateGrade(double distance, double elevationDifference) {
distanceBuffer.setNext(distance);
double smoothedDistance = distanceBuffer.getAverage();
// With the error in the altitude measurement it is dangerous to divide
// by anything less than 5.
if (!elevationBuffer.isFull() || !distanceBuffer.isFull()
|| smoothedDistance < 5.0) {
return; return;
} }
currentGrade = elevationDifference / smoothedDistance;
gradeBuffer.setNext(currentGrade); tripStatistics.setStartTime(time);
data.updateGradeExtremities(gradeBuffer.getAverage()); tripStatistics.setStopTime(-1L);
paused = false;
lastLocation = null;
lastMovingLocation = null;
speedBuffer.reset();
elevationBuffer.reset();
distanceBuffer.reset();
gradeBuffer.reset();
} }
/** /**
* Pauses the track at the given time. * Pauses the track at a given time.
* *
* @param time the time to pause at * @param time the time to pause at
*/ */
public void pauseAt(long time) { public void pauseAt(long time) {
if (paused) { return; } if (paused) {
return;
data.setStopTime(time); }
data.setTotalTime(time - data.getStartTime()); tripStatistics.setStopTime(time);
lastLocation = null; // Make sure the counter restarts. // TODO: total time needs to take into account pauses
tripStatistics.setTotalTime(time - tripStatistics.getStartTime());
paused = true; paused = true;
} }
/** /**
* Resumes the current track at the given time. * Gets the trip statistics.
*
* @param time the time to resume at
*/ */
public void resumeAt(long time) { public TripStatistics getTripStatistics() {
if (!paused) { return; } // Take a snapshot - we don't want anyone messing with our internals
return new TripStatistics(tripStatistics);
// TODO: The times are bogus if the track is paused then resumed again
data.setStartTime(time);
data.setStopTime(-1);
paused = false;
}
@Override
public String toString() {
return "TripStatistics { Data: " + data.toString()
+ "; Total Locations: " + totalLocations
+ "; Paused: " + paused
+ "; Current speed: " + currentSpeed
+ "; Current grade: " + currentGrade
+ "}";
} }
/** /**
* Returns the amount of time the user has been idle or 0 if they are moving. * Returns the amount of time the user has been idle or 0 if he is moving.
*/ */
public long getIdleTime() { public long getIdleTime() {
if (lastLocation == null || lastMovingLocation == null) if (lastLocation == null || lastMovingLocation == null) {
return 0; return 0;
}
return lastLocation.getTime() - lastMovingLocation.getTime(); return lastLocation.getTime() - lastMovingLocation.getTime();
} }
/** /**
* Gets the current elevation smoothed over several readings. The elevation * Gets the smoothed elevation over several readings. The elevation readings
* data is very noisy so it is better to use the smoothed elevation than the * is noisy so the smoothed elevation is better than the raw elevation for
* raw elevation for many tasks. * many tasks.
*
* @return The elevation smoothed over several readings
*/ */
public double getSmoothedElevation() { public double getSmoothedElevation() {
return elevationBuffer.getAverage(); return elevationBuffer.getAverage();
} }
public TripStatistics getStatistics() { /**
// Take a snapshot - we don't want anyone messing with our internals * Adds a location. This will update all of the internal variables with this
return new TripStatistics(data); * new location.
*
* @param location the location
* @param systemTime the system time for calculating totalTime. This should be
* the phone's system time (not GPS time)
* @return true if the person is moving
*/
public boolean addLocation(Location location, long systemTime) {
if (paused) {
Log.w(TAG, "Track is paused. Ignore addLocation.");
return false;
}
tripStatistics.setTotalTime(systemTime - tripStatistics.getStartTime());
double elevationDifference = updateElevation(location.getAltitude());
tripStatistics.updateLatitudeExtremities(location.getLatitude());
tripStatistics.updateLongitudeExtremities(location.getLongitude());
// If this is the first location, remember it and return.
if (lastLocation == null || lastMovingLocation == null) {
lastLocation = location;
lastMovingLocation = location;
return false;
}
// Don't do anything more if we didn't move since the last location.
double distance = lastLocation.distanceTo(location);
if (distance < minRecordingDistance && location.getSpeed() < Constants.MAX_NO_MOVEMENT_SPEED) {
lastLocation = location;
return false;
}
long timeDifference = location.getTime() - lastLocation.getTime();
if (timeDifference < 0) {
Log.e(TAG, "Negative time difference: " + timeDifference);
lastLocation = location;
return false;
}
tripStatistics.addTotalDistance(lastMovingLocation.distanceTo(location));
tripStatistics.addMovingTime(timeDifference);
updateSpeed(
location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
updateGrade(distance, elevationDifference);
lastLocation = location;
lastMovingLocation = location;
return true;
} }
public void setMinRecordingDistance(int minRecordingDistance) { /**
this.minRecordingDistance = minRecordingDistance; * Updates a speed reading. Assumes the user is moving.
*
* @param time the time
* @param speed the speed
* @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed
*/
@VisibleForTesting
void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
if (!isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed, speedBuffer)) {
Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed);
return;
}
speedBuffer.setNext(speed);
if (speed > tripStatistics.getMaxSpeed()) {
tripStatistics.setMaxSpeed(speed);
}
double movingSpeed = tripStatistics.getAverageMovingSpeed();
if (speedBuffer.isFull() && movingSpeed > tripStatistics.getMaxSpeed()) {
tripStatistics.setMaxSpeed(movingSpeed);
}
}
/**
* Updates an elevation reading.
*
* @param elevation the elevation
*/
@VisibleForTesting
double updateElevation(double elevation) {
double oldAverage = elevationBuffer.getAverage();
elevationBuffer.setNext(elevation);
double newAverage = elevationBuffer.getAverage();
tripStatistics.updateElevationExtremities(newAverage);
double elevationDifference = elevationBuffer.isFull() ? newAverage - oldAverage : 0.0;
if (elevationDifference > 0) {
tripStatistics.addTotalElevationGain(elevationDifference);
}
return elevationDifference;
}
/**
* Updates a grade reading.
*
* @param distance the distance the user just traveled
* @param elevationDifference the elevation difference between the current
* reading and the previous reading
*/
@VisibleForTesting
void updateGrade(double distance, double elevationDifference) {
distanceBuffer.setNext(distance);
double smoothedDistance = distanceBuffer.getAverage();
/*
* With the error in the altitude measurement it is dangerous to divide by
* anything less than 5.
*/
if (!elevationBuffer.isFull() || !distanceBuffer.isFull() || smoothedDistance < 5.0) {
return;
}
gradeBuffer.setNext(elevationDifference / smoothedDistance);
tripStatistics.updateGradeExtremities(gradeBuffer.getAverage());
}
/**
* Returns true if the speed is valid.
*
* @param time the time
* @param speed the speed
* @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed
* @param speedBuffer a buffer of speed readings
*/
public static boolean isValidSpeed(long time, double speed, long lastLocationTime,
double lastLocationSpeed, DoubleBuffer speedBuffer) {
/*
* There are a lot of noisy speed readings. Do the cheapest checks first,
* most expensive last.
*/
if (speed == 0) {
return false;
}
/*
* The following code will ignore unlikely readings. 128 m/s seems to be an
* internal android error code.
*/
if (Math.abs(speed - 128) < 1) {
return false;
}
/*
* See if the speed seems physically likely. Ignore any speeds that imply
* acceleration greater than 2g.
*/
long timeDifference = time - lastLocationTime;
double speedDifference = Math.abs(lastLocationSpeed - speed);
if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) {
return false;
}
/*
* Only check if the speed buffer is full. Check that the speed is less than
* 10X the smoothed average and the speed difference doesn't imply 2g
* acceleration.
*/
if (!speedBuffer.isFull()) {
return true;
}
double average = speedBuffer.getAverage();
double diff = Math.abs(average - speed);
return (speed < average * 10) && (diff < Constants.MAX_ACCELERATION * timeDifference);
} }
} }
@@ -23,14 +23,10 @@ package com.google.android.apps.mytracks.stats;
*/ */
public class ExtremityMonitor { public class ExtremityMonitor {
/** // The smallest value seen so far.
* The smallest value seen so far.
*/
private double min; private double min;
/** // The largest value seen so far.
* The largest value seen so far.
*/
private double max; private double max;
public ExtremityMonitor() { public ExtremityMonitor() {
@@ -38,9 +34,31 @@ public class ExtremityMonitor {
} }
/** /**
* Updates the min and the max with the new value. * Resets this object to it's initial state where the min and max are unknown.
*/
public void reset() {
min = Double.POSITIVE_INFINITY;
max = Double.NEGATIVE_INFINITY;
}
/**
* Gets the minimum value seen.
*/
public double getMin() {
return min;
}
/**
* Gets the maximum value seen.
*/
public double getMax() {
return max;
}
/**
* Updates the min and the max with a new value.
* *
* @param value the new value for the monitor * @param value the new value
* @return true if an extremity was found * @return true if an extremity was found
*/ */
public boolean update(double value) { public boolean update(double value) {
@@ -56,34 +74,11 @@ public class ExtremityMonitor {
return changed; return changed;
} }
/**
* Gets the minimum value seen.
*
* @return The minimum value passed into the update() function
*/
public double getMin() {
return min;
}
/**
* Gets the maximum value seen.
*
* @return The maximum value passed into the update() function
*/
public double getMax() {
return max;
}
/**
* Resets this object to it's initial state where the min and max are unknown.
*/
public void reset() {
min = Double.POSITIVE_INFINITY;
max = Double.NEGATIVE_INFINITY;
}
/** /**
* Sets the minimum and maximum values. * Sets the minimum and maximum values.
*
* @param min the minimum value
* @param max the maximum value
*/ */
public void set(double min, double max) { public void set(double min, double max) {
this.min = min; this.min = min;
@@ -92,6 +87,8 @@ public class ExtremityMonitor {
/** /**
* Sets the minimum value. * Sets the minimum value.
*
* @param min the minimum value
*/ */
public void setMin(double min) { public void setMin(double min) {
this.min = min; this.min = min;
@@ -99,14 +96,18 @@ public class ExtremityMonitor {
/** /**
* Sets the maximum value. * Sets the maximum value.
*
* @param max the maximum value
*/ */
public void setMax(double max) { public void setMax(double max) {
this.max = max; this.max = max;
} }
/**
* Returns true if has data.
*/
public boolean hasData() { public boolean hasData() {
return min != Double.POSITIVE_INFINITY return min != Double.POSITIVE_INFINITY && max != Double.NEGATIVE_INFINITY;
&& max != Double.NEGATIVE_INFINITY;
} }
@Override @Override
@@ -13,85 +13,59 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.stats; package com.google.android.apps.mytracks.stats;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
/** /**
* Statistical data about a trip. * Statistical data about a trip. The data in this class should be filled out by
* The data in this class should be filled out by TripStatisticsBuilder. * TripStatisticsBuilder.
* * <p>
* TODO: hashCode and equals * TODO: hashCode and equals
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TripStatistics implements Parcelable { public class TripStatistics implements Parcelable {
/** // The trip start time. This is the system time, might not match the GPs time.
* The start time for the trip. This is system time which might not match gps
* time.
*/
private long startTime = -1L; private long startTime = -1L;
/** // The trip stop time. This is the system time, might not match the GPS time.
* The stop time for the trip. This is the system time which might not match
* gps time.
*/
private long stopTime = -1L; private long stopTime = -1L;
/** // The total trip distance (meters).
* The total time that we believe the user was traveling in milliseconds.
*/
private long movingTime;
/**
* The total time of the trip in milliseconds.
* This is only updated when new points are received, so it may be stale.
*/
private long totalTime;
/**
* The total distance in meters that the user traveled on this trip.
*/
private double totalDistance; private double totalDistance;
/** // The total time (ms). Updated when new points are received, may be stale.
* The total elevation gained on this trip in meters. private long totalTime;
*/
private double totalElevationGain;
/** // The total moving time (ms). Based on when we believe the user is traveling.
* The maximum speed in meters/second reported that we believe to be a valid private long movingTime;
* speed.
*/
private double maxSpeed;
/** // The min and max latitude seen in this trip.
* The min and max latitude values seen in this trip.
*/
private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
/** // The min and max longitude seen in this trip.
* The min and max longitude values seen in this trip.
*/
private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor(); private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor();
/** // The maximum speed (meters/second) that we believe is valid.
* The min and max elevation seen on this trip in meters. private double maxSpeed;
*/
// The min and max elevation (meters) seen on this trip.
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
/** // The total elevation gained (meters).
* The minimum and maximum grade calculations on this trip. private double totalElevationGain;
*/
// The min and max grade seen on this trip.
private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); private final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
/** /**
* Default constructor. * Default constructor.
*/ */
public TripStatistics() { public TripStatistics() {}
}
/** /**
* Copy constructor. * Copy constructor.
@@ -99,96 +73,166 @@ public class TripStatistics implements Parcelable {
* @param other another statistics data object to copy from * @param other another statistics data object to copy from
*/ */
public TripStatistics(TripStatistics other) { public TripStatistics(TripStatistics other) {
this.maxSpeed = other.maxSpeed;
this.movingTime = other.movingTime;
this.startTime = other.startTime; this.startTime = other.startTime;
this.stopTime = other.stopTime; this.stopTime = other.stopTime;
this.totalDistance = other.totalDistance; this.totalDistance = other.totalDistance;
this.totalElevationGain = other.totalElevationGain;
this.totalTime = other.totalTime; this.totalTime = other.totalTime;
this.movingTime = other.movingTime;
this.latitudeExtremities.set(other.latitudeExtremities.getMin(), this.latitudeExtremities.set(
other.latitudeExtremities.getMax()); other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax());
this.longitudeExtremities.set(other.longitudeExtremities.getMin(), this.longitudeExtremities.set(
other.longitudeExtremities.getMax()); other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax());
this.elevationExtremities.set(other.elevationExtremities.getMin(), this.maxSpeed = other.maxSpeed;
other.elevationExtremities.getMax()); this.elevationExtremities.set(
this.gradeExtremities.set(other.gradeExtremities.getMin(), other.elevationExtremities.getMin(), other.elevationExtremities.getMax());
other.gradeExtremities.getMax()); this.totalElevationGain = other.totalElevationGain;
this.gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax());
} }
/** /**
* Combines these statistics with those from another object. * Combines these statistics with those from another object. This assumes that
* This assumes that the time periods covered by each do not intersect. * the time periods covered by each do not intersect.
* *
* @param other the other waypoint * @param other another statistics data object
*/ */
public void merge(TripStatistics other) { public void merge(TripStatistics other) {
startTime = Math.min(startTime, other.startTime); startTime = Math.min(startTime, other.startTime);
stopTime = Math.max(stopTime, other.stopTime); stopTime = Math.max(stopTime, other.stopTime);
totalDistance += other.totalDistance;
totalTime += other.totalTime; totalTime += other.totalTime;
movingTime += other.movingTime; movingTime += other.movingTime;
totalDistance += other.totalDistance;
totalElevationGain += other.totalElevationGain;
maxSpeed = Math.max(maxSpeed, other.maxSpeed);
latitudeExtremities.update(other.latitudeExtremities.getMax());
latitudeExtremities.update(other.latitudeExtremities.getMin()); latitudeExtremities.update(other.latitudeExtremities.getMin());
longitudeExtremities.update(other.longitudeExtremities.getMax()); latitudeExtremities.update(other.latitudeExtremities.getMax());
longitudeExtremities.update(other.longitudeExtremities.getMin()); longitudeExtremities.update(other.longitudeExtremities.getMin());
elevationExtremities.update(other.elevationExtremities.getMax()); longitudeExtremities.update(other.longitudeExtremities.getMax());
maxSpeed = Math.max(maxSpeed, other.maxSpeed);
elevationExtremities.update(other.elevationExtremities.getMin()); elevationExtremities.update(other.elevationExtremities.getMin());
gradeExtremities.update(other.gradeExtremities.getMax()); elevationExtremities.update(other.elevationExtremities.getMax());
totalElevationGain += other.totalElevationGain;
gradeExtremities.update(other.gradeExtremities.getMin()); gradeExtremities.update(other.gradeExtremities.getMin());
gradeExtremities.update(other.gradeExtremities.getMax());
} }
/** /**
* Gets the time that this track started. * Gets the trip start time. The number of milliseconds since epoch.
*
* @return The number of milliseconds since epoch to the time when this track
* started
*/ */
public long getStartTime() { public long getStartTime() {
return startTime; return startTime;
} }
/** /**
* Gets the time that this track stopped. * Gets the trip stop time. The number of milliseconds since epoch.
*
* @return The number of milliseconds since epoch to the time when this track
* stopped
*/ */
public long getStopTime() { public long getStopTime() {
return stopTime; return stopTime;
} }
/** /**
* Gets the total time that this track has been active. * Gets the total distance the user traveled in meters.
* This statistic is only updated when a new point is added to the statistics,
* so it may be off. If you need to calculate the proper total time, use
* {@link #getStartTime} with the current time.
*
* @return The total number of milliseconds the track was active
*/
public long getTotalTime() {
return totalTime;
}
/**
* Gets the total distance the user traveled.
*
* @return The total distance traveled in meters
*/ */
public double getTotalDistance() { public double getTotalDistance() {
return totalDistance; return totalDistance;
} }
/** /**
* Gets the the average speed the user traveled. * Gets the total time in milliseconds that this track has been active. This
* This calculation only takes into account the displacement until the last * statistic is only updated when a new point is added to the statistics, so
* point that was accounted for in statistics. * it may be off. If you need to calculate the proper total time, use
* * {@link #getStartTime} with the current time.
* @return The average speed in m/s */
public long getTotalTime() {
return totalTime;
}
/**
* Gets the moving time in milliseconds.
*/
public long getMovingTime() {
return movingTime;
}
/**
* Gets the topmost position (highest latitude) of the track, in signed
* degrees.
*/
public double getTopDegrees() {
return latitudeExtremities.getMax();
}
/**
* Gets the topmost position (highest latitude) of the track, in signed
* millions of degrees.
*/
public int getTop() {
return (int) (latitudeExtremities.getMax() * 1E6);
}
/**
* Gets the bottommost position (lowest latitude) of the track, in signed
* degrees.
*/
public double getBottomDegrees() {
return latitudeExtremities.getMin();
}
/**
* Gets the bottommost position (lowest latitude) of the track, in signed
* millions of degrees.
*/
public int getBottom() {
return (int) (latitudeExtremities.getMin() * 1E6);
}
/**
* Gets the leftmost position (lowest longitude) of the track, in signed
* degrees.
*/
public double getLeftDegrees() {
return longitudeExtremities.getMin();
}
/**
* Gets the leftmost position (lowest longitude) of the track, in signed
* millions of degrees.
*/
public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6);
}
/**
* Gets the rightmost position (highest longitude) of the track, in signed
* degrees.
*/
public double getRightDegrees() {
return longitudeExtremities.getMax();
}
/**
* Gets the rightmost position (highest longitude) of the track, in signed
* millions of degrees.
*/
public int getRight() {
return (int) (longitudeExtremities.getMax() * 1E6);
}
/**
* Gets the mean latitude position of the track, in signed degrees.
*/
public double getMeanLatitude() {
return (getBottomDegrees() + getTopDegrees()) / 2.0;
}
/**
* Gets the mean longitude position of the track, in signed degrees.
*/
public double getMeanLongitude() {
return (getLeftDegrees() + getRightDegrees()) / 2.0;
}
/**
* Gets the average speed in meters/second. This calculation only takes into
* account the displacement until the last point that was accounted for in
* statistics.
*/ */
public double getAverageSpeed() { public double getAverageSpeed() {
if (totalTime == 0L) { if (totalTime == 0L) {
@@ -198,10 +242,7 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the the average speed the user traveled when they were actively * Gets the average moving speed in meters/second.
* moving.
*
* @return The average moving speed in m/s
*/ */
public double getAverageMovingSpeed() { public double getAverageMovingSpeed() {
if (movingTime == 0L) { if (movingTime == 0L) {
@@ -211,179 +252,68 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the the maximum speed for this track. * Gets the maximum speed in meters/second.
*
* @return The maximum speed in m/s
*/ */
public double getMaxSpeed() { public double getMaxSpeed() {
return maxSpeed; return maxSpeed;
} }
/** /**
* Gets the moving time. * Gets the minimum elevation. This is calculated from the smoothed elevation
* * so this can actually be more than the current elevation.
* @return The total number of milliseconds the user was moving
*/
public long getMovingTime() {
return movingTime;
}
/**
* Gets the total elevation gain for this trip. This is calculated as the sum
* of all positive differences in the smoothed elevation.
*
* @return The elevation gain in meters for this trip
*/
public double getTotalElevationGain() {
return totalElevationGain;
}
/**
* Returns the leftmost position (lowest longitude) of the track, in signed degrees.
*/
public double getLeftDegrees() {
return longitudeExtremities.getMin();
}
/**
* Returns the leftmost position (lowest longitude) of the track, in signed millions of degrees.
*/
public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6);
}
/**
* Returns the rightmost position (highest longitude) of the track, in signed degrees.
*/
public double getRightDegrees() {
return longitudeExtremities.getMax();
}
/**
* Returns the rightmost position (highest longitude) of the track, in signed millions of degrees.
*/
public int getRight() {
return (int) (longitudeExtremities.getMax() * 1E6);
}
/**
* Returns the bottommost position (lowest latitude) of the track, in signed degrees.
*/
public double getBottomDegrees() {
return latitudeExtremities.getMin();
}
/**
* Returns the bottommost position (lowest latitude) of the track, in signed millions of degrees.
*/
public int getBottom() {
return (int) (latitudeExtremities.getMin() * 1E6);
}
/**
* Returns the topmost position (highest latitude) of the track, in signed degrees.
*/
public double getTopDegrees() {
return latitudeExtremities.getMax();
}
/**
* Returns the topmost position (highest latitude) of the track, in signed millions of degrees.
*/
public int getTop() {
return (int) (latitudeExtremities.getMax() * 1E6);
}
/**
* Returns the mean position (center latitude) of the track, in signed degrees.
*/
public double getMeanLatitude() {
return (getBottomDegrees() + getTopDegrees()) / 2.0;
}
/**
* Returns the mean position (center longitude) of the track, in signed degrees.
*/
public double getMeanLongitude() {
return (getLeftDegrees() + getRightDegrees()) / 2.0;
}
/**
* Gets the minimum elevation seen on this trip. This is calculated from the
* smoothed elevation so this can actually be more than the current elevation.
*
* @return The smallest elevation reading for this trip in meters
*/ */
public double getMinElevation() { public double getMinElevation() {
return elevationExtremities.getMin(); return elevationExtremities.getMin();
} }
/** /**
* Gets the maximum elevation seen on this trip. This is calculated from the * Gets the maximum elevation. This is calculated from the smoothed elevation
* smoothed elevation so this can actually be less than the current elevation. * so this can actually be less than the current elevation.
*
* @return The largest elevation reading for this trip in meters
*/ */
public double getMaxElevation() { public double getMaxElevation() {
return elevationExtremities.getMax(); return elevationExtremities.getMax();
} }
/**
* Gets the total elevation gain in meters. This is calculated as the sum of
* all positive differences in the smoothed elevation.
*/
public double getTotalElevationGain() {
return totalElevationGain;
}
/**
* Gets the minimum grade for this trip.
*/
public double getMinGrade() {
return gradeExtremities.getMin();
}
/** /**
* Gets the maximum grade for this trip. * Gets the maximum grade for this trip.
*
* @return The maximum grade for this trip as a fraction
*/ */
public double getMaxGrade() { public double getMaxGrade() {
return gradeExtremities.getMax(); return gradeExtremities.getMax();
} }
/** /**
* Gets the minimum grade for this trip. * Sets the trip start time.
* *
* @return The minimum grade for this trip as a fraction * @param startTime the trip start time in milliseconds since the epoch
*/
public double getMinGrade() {
return gradeExtremities.getMin();
}
// Setters - to be used when restoring state or loading from the DB
/**
* Sets the start time for this trip.
*
* @param startTime the start time, in milliseconds since the epoch
*/ */
public void setStartTime(long startTime) { public void setStartTime(long startTime) {
this.startTime = startTime; this.startTime = startTime;
} }
/** /**
* Sets the stop time for this trip. * Sets the trip stop time.
* *
* @param stopTime the stop time, in milliseconds since the epoch * @param stopTime the stop time in milliseconds since the epoch
*/ */
public void setStopTime(long stopTime) { public void setStopTime(long stopTime) {
this.stopTime = stopTime; this.stopTime = stopTime;
} }
/**
* Sets the total moving time.
*
* @param movingTime the moving time in milliseconds
*/
public void setMovingTime(long movingTime) {
this.movingTime = movingTime;
}
/**
* Sets the total trip time.
*
* @param totalTime the total trip time in milliseconds
*/
public void setTotalTime(long totalTime) {
this.totalTime = totalTime;
}
/** /**
* Sets the total trip distance. * Sets the total trip distance.
* *
@@ -393,76 +323,6 @@ public class TripStatistics implements Parcelable {
this.totalDistance = totalDistance; this.totalDistance = totalDistance;
} }
/**
* Sets the total elevation variation during the trip.
*
* @param totalElevationGain the elevation variation in meters
*/
public void setTotalElevationGain(double totalElevationGain) {
this.totalElevationGain = totalElevationGain;
}
/**
* Sets the maximum speed reached during the trip.
*
* @param maxSpeed the maximum speed in meters per second
*/
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed = maxSpeed;
}
/**
* Sets the minimum elevation reached during the trip.
*
* @param elevation the minimum elevation in meters
*/
public void setMinElevation(double elevation) {
elevationExtremities.setMin(elevation);
}
/**
* Sets the maximum elevation reached during the trip.
*
* @param elevation the maximum elevation in meters
*/
public void setMaxElevation(double elevation) {
elevationExtremities.setMax(elevation);
}
/**
* Sets the minimum grade obtained during the trip.
*
* @param grade the grade as a fraction (-1.0 would mean vertical downwards)
*/
public void setMinGrade(double grade) {
gradeExtremities.setMin(grade);
}
/**
* Sets the maximum grade obtained during the trip).
*
* @param grade the grade as a fraction (1.0 would mean vertical upwards)
*/
public void setMaxGrade(double grade) {
gradeExtremities.setMax(grade);
}
/**
* Sets the bounding box for this trip.
* The unit for all parameters is signed decimal degrees (degrees * 1E6).
*
* @param leftE6 the westmost longitude reached
* @param topE6 the northmost latitude reached
* @param rightE6 the eastmost longitude reached
* @param bottomE6 the southmost latitude reached
*/
public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) {
latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6);
longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6);
}
// Data manipulation methods
/** /**
* Adds to the current total distance. * Adds to the current total distance.
* *
@@ -473,16 +333,25 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Adds to the total elevation variation. * Sets the trip total time.
* *
* @param gain the elevation variation in meters * @param totalTime the trip total time in milliseconds
*/ */
void addTotalElevationGain(double gain) { public void setTotalTime(long totalTime) {
totalElevationGain += gain; this.totalTime = totalTime;
} }
/** /**
* Adds to the total moving time of the trip. * Sets the trip total moving time.
*
* @param movingTime the trip total moving time in milliseconds
*/
public void setMovingTime(long movingTime) {
this.movingTime = movingTime;
}
/**
* Adds to the trip total moving time.
* *
* @param time the time in milliseconds * @param time the time in milliseconds
*/ */
@@ -491,7 +360,21 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Accounts for a new latitude value for the bounding box. * Sets the bounding box for this trip. The unit for all parameters is signed
* millions of degree (degrees * 1E6).
*
* @param leftE6 the leftmost longitude reached
* @param topE6 the topmost latitude reached
* @param rightE6 the rightmost longitude reached
* @param bottomE6 the bottommost latitude reached
*/
public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) {
latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6);
longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6);
}
/**
* Updates a new latitude value.
* *
* @param latitude the latitude value in signed decimal degrees * @param latitude the latitude value in signed decimal degrees
*/ */
@@ -500,7 +383,7 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Accounts for a new longitude value for the bounding box. * Updates a new longitude value.
* *
* @param longitude the longitude value in signed decimal degrees * @param longitude the longitude value in signed decimal degrees
*/ */
@@ -509,7 +392,34 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Accounts for a new elevation value for the bounding box. * Sets the maximum speed.
*
* @param maxSpeed the maximum speed in meters/second
*/
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed = maxSpeed;
}
/**
* Sets the minimum elevation.
*
* @param elevation the minimum elevation in meters
*/
public void setMinElevation(double elevation) {
elevationExtremities.setMin(elevation);
}
/**
* Sets the maximum elevation.
*
* @param elevation the maximum elevation in meters
*/
public void setMaxElevation(double elevation) {
elevationExtremities.setMax(elevation);
}
/**
* Updates a new elevation.
* *
* @param elevation the elevation value in meters * @param elevation the elevation value in meters
*/ */
@@ -518,7 +428,43 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Accounts for a new grade value. * Sets the total elevation gain.
*
* @param totalElevationGain the elevation gain in meters
*/
public void setTotalElevationGain(double totalElevationGain) {
this.totalElevationGain = totalElevationGain;
}
/**
* Adds to the total elevation gain.
*
* @param gain the elevation gain in meters
*/
void addTotalElevationGain(double gain) {
totalElevationGain += gain;
}
/**
* Sets the minimum grade.
*
* @param grade the grade as a fraction (-1.0 would mean vertical downwards)
*/
public void setMinGrade(double grade) {
gradeExtremities.setMin(grade);
}
/**
* Sets the maximum grade.
*
* @param grade the grade as a fraction (1.0 would mean vertical upwards)
*/
public void setMaxGrade(double grade) {
gradeExtremities.setMax(grade);
}
/**
* Updates a new grade value.
* *
* @param grade the grade value as a fraction * @param grade the grade value as a fraction
*/ */
@@ -526,41 +472,33 @@ public class TripStatistics implements Parcelable {
gradeExtremities.update(grade); gradeExtremities.update(grade);
} }
// String conversion
@Override @Override
public String toString() { public String toString() {
return "TripStatistics { Start Time: " + getStartTime() return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime()
+ "; Total Time: " + getTotalTime() + "; Total Time: " + getTotalTime() + "; Total Distance: " + getTotalDistance()
+ "; Moving Time: " + getMovingTime() + "; Total Time: " + getTotalTime() + "; Moving Time: " + getMovingTime()
+ "; Total Distance: " + getTotalDistance() + "; Min Latitude: " + getBottomDegrees() + "; Max Latitude: " + getTopDegrees()
+ "; Elevation Gain: " + getTotalElevationGain() + "; Min Longitude: " + getLeftDegrees() + "; Max Longitude: " + getRightDegrees()
+ "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation() + "; Max Speed: " + getMaxSpeed()
+ "; Max Elevation: " + getMaxElevation() + "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation()
+ "; Average Speed: " + getAverageMovingSpeed() + "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade()
+ "; Min Grade: " + getMinGrade() + "; Max Grade: " + getMaxGrade() + "}";
+ "; Max Grade: " + getMaxGrade()
+ "}";
} }
// Parcelable interface and creator
/** /**
* Creator of statistics data from parcels. * Creator of statistics data from parcels.
*/ */
public static class Creator public static class Creator implements Parcelable.Creator<TripStatistics> {
implements Parcelable.Creator<TripStatistics> {
@Override @Override
public TripStatistics createFromParcel(Parcel source) { public TripStatistics createFromParcel(Parcel source) {
TripStatistics data = new TripStatistics(); TripStatistics data = new TripStatistics();
data.startTime = source.readLong(); data.startTime = source.readLong();
data.movingTime = source.readLong(); data.stopTime = source.readLong();
data.totalTime = source.readLong();
data.totalDistance = source.readDouble(); data.totalDistance = source.readDouble();
data.totalElevationGain = source.readDouble(); data.totalTime = source.readLong();
data.maxSpeed = source.readDouble(); data.movingTime = source.readLong();
double minLat = source.readDouble(); double minLat = source.readDouble();
double maxLat = source.readDouble(); double maxLat = source.readDouble();
@@ -570,9 +508,12 @@ public class TripStatistics implements Parcelable {
double maxLong = source.readDouble(); double maxLong = source.readDouble();
data.longitudeExtremities.set(minLong, maxLong); data.longitudeExtremities.set(minLong, maxLong);
data.maxSpeed = source.readDouble();
double minElev = source.readDouble(); double minElev = source.readDouble();
double maxElev = source.readDouble(); double maxElev = source.readDouble();
data.elevationExtremities.set(minElev, maxElev); data.elevationExtremities.set(minElev, maxElev);
data.totalElevationGain = source.readDouble();
double minGrade = source.readDouble(); double minGrade = source.readDouble();
double maxGrade = source.readDouble(); double maxGrade = source.readDouble();
@@ -600,18 +541,18 @@ public class TripStatistics implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(startTime); dest.writeLong(startTime);
dest.writeLong(movingTime); dest.writeLong(stopTime);
dest.writeLong(totalTime);
dest.writeDouble(totalDistance); dest.writeDouble(totalDistance);
dest.writeDouble(totalElevationGain); dest.writeLong(totalTime);
dest.writeDouble(maxSpeed); dest.writeLong(movingTime);
dest.writeDouble(latitudeExtremities.getMin()); dest.writeDouble(latitudeExtremities.getMin());
dest.writeDouble(latitudeExtremities.getMax()); dest.writeDouble(latitudeExtremities.getMax());
dest.writeDouble(longitudeExtremities.getMin()); dest.writeDouble(longitudeExtremities.getMin());
dest.writeDouble(longitudeExtremities.getMax()); dest.writeDouble(longitudeExtremities.getMax());
dest.writeDouble(maxSpeed);
dest.writeDouble(elevationExtremities.getMin()); dest.writeDouble(elevationExtremities.getMin());
dest.writeDouble(elevationExtremities.getMax()); dest.writeDouble(elevationExtremities.getMax());
dest.writeDouble(totalElevationGain);
dest.writeDouble(gradeExtremities.getMin()); dest.writeDouble(gradeExtremities.getMin());
dest.writeDouble(gradeExtremities.getMax()); dest.writeDouble(gradeExtremities.getMax());
} }
@@ -25,55 +25,49 @@ public class TripStatisticsBuilderTest extends TestCase {
public void testAddLocationSimple() throws Exception { public void testAddLocationSimple() throws Exception {
builder = new TripStatisticsBuilder(1000); builder = new TripStatisticsBuilder(1000);
TripStatistics stats = builder.getStatistics(); TripStatistics tripStatistics = builder.getTripStatistics();
assertEquals(0.0, builder.getSmoothedElevation()); assertEquals(0.0, builder.getSmoothedElevation());
assertEquals(Double.POSITIVE_INFINITY, stats.getMinElevation()); assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinElevation());
assertEquals(Double.NEGATIVE_INFINITY, stats.getMaxElevation()); assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxElevation());
assertEquals(0.0, stats.getMaxSpeed()); assertEquals(0.0, tripStatistics.getMaxSpeed());
assertEquals(Double.POSITIVE_INFINITY, stats.getMinGrade()); assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinGrade());
assertEquals(Double.NEGATIVE_INFINITY, stats.getMaxGrade()); assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxGrade());
assertEquals(0.0, stats.getTotalElevationGain()); assertEquals(0.0, tripStatistics.getTotalElevationGain());
assertEquals(0, stats.getMovingTime()); assertEquals(0, tripStatistics.getMovingTime());
assertEquals(0.0, stats.getTotalDistance()); assertEquals(0.0, tripStatistics.getTotalDistance());
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
Location l = new Location("test"); Location location = new Location("test");
l.setAccuracy(1.0f); location.setAccuracy(1.0f);
l.setLongitude(45.0); location.setLongitude(45.0);
// Going up by 5 meters each time. // Going up by 1 meter each time.
l.setAltitude(i); location.setAltitude(i);
// Moving by .1% of a degree latitude. location.setLatitude(i * .001); // Moving by .001 degree latitude (111 meters)
l.setLatitude(i * .001); location.setSpeed(11.1f);
l.setSpeed(11.1f);
// Each time slice is 10 seconds. // Each time slice is 10 seconds.
long time = 1000 + 10000 * i; long time = 1000 + 10000 * i;
l.setTime(time); location.setTime(time);
boolean moving = builder.addLocation(l, time); boolean moving = builder.addLocation(location, time);
assertEquals((i != 0), moving); assertEquals((i != 0), moving);
stats = builder.getStatistics(); tripStatistics = builder.getTripStatistics();
assertEquals(10000 * i, stats.getTotalTime()); assertEquals(10000 * i, tripStatistics.getTotalTime());
assertEquals(10000 * i, stats.getMovingTime()); assertEquals(10000 * i, tripStatistics.getMovingTime());
assertEquals(i, builder.getSmoothedElevation(), assertEquals(i, builder.getSmoothedElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2);
Constants.ELEVATION_SMOOTHING_FACTOR / 2); assertEquals(0.0, tripStatistics.getMinElevation());
assertEquals(0.0, stats.getMinElevation()); assertEquals(i, tripStatistics.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(i, stats.getMaxElevation(), assertEquals(i, tripStatistics.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR);
Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(i, stats.getTotalElevationGain(),
Constants.ELEVATION_SMOOTHING_FACTOR);
if (i > Constants.SPEED_SMOOTHING_FACTOR) { if (i > Constants.SPEED_SMOOTHING_FACTOR) {
assertEquals(11.1f, stats.getMaxSpeed(), 0.1); assertEquals(11.1f, tripStatistics.getMaxSpeed(), 0.1);
} }
if ((i > Constants.GRADE_SMOOTHING_FACTOR) if (i > Constants.DISTANCE_SMOOTHING_FACTOR && i > Constants.ELEVATION_SMOOTHING_FACTOR) {
&& (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { // 1 m / 111 m = .009
assertEquals(0.009, stats.getMinGrade(), 0.0001); assertEquals(0.009, tripStatistics.getMinGrade(), 0.0001);
assertEquals(0.009, stats.getMaxGrade(), 0.0001); assertEquals(0.009, tripStatistics.getMaxGrade(), 0.0001);
} }
// 1 degree = 111 km assertEquals(111.0 * i, tripStatistics.getTotalDistance(), 100);
// 1 timeslice = 0.001 degree = 111 m
assertEquals(111.0 * i, stats.getTotalDistance(), 100);
} }
} }
@@ -86,7 +80,7 @@ public class TripStatisticsBuilderTest extends TestCase {
for (int j = 0; j < 100; j++) { for (int j = 0; j < 100; j++) {
assertEquals(0.0, builder.updateElevation(elevation)); assertEquals(0.0, builder.updateElevation(elevation));
assertEquals(elevation, builder.getSmoothedElevation()); assertEquals(elevation, builder.getSmoothedElevation());
TripStatistics data = builder.getStatistics(); TripStatistics data = builder.getTripStatistics();
assertEquals(elevation, data.getMinElevation()); assertEquals(elevation, data.getMinElevation());
assertEquals(elevation, data.getMaxElevation()); assertEquals(elevation, data.getMaxElevation());
assertEquals(0.0, data.getTotalElevationGain()); assertEquals(0.0, data.getTotalElevationGain());
@@ -104,80 +98,71 @@ public class TripStatisticsBuilderTest extends TestCase {
} else { } else {
expectedGain = 1.0; expectedGain = 1.0;
} }
assertEquals(expectedGain, assertEquals(expectedGain, builder.updateElevation(i));
builder.updateElevation(i));
assertEquals(i, builder.getSmoothedElevation(), 20); assertEquals(i, builder.getSmoothedElevation(), 20);
TripStatistics data = builder.getStatistics(); TripStatistics data = builder.getTripStatistics();
assertEquals(0.0, data.getMinElevation(), 0.0); assertEquals(0.0, data.getMinElevation(), 0.0);
assertEquals(i, data.getMaxElevation(), assertEquals(i, data.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR);
Constants.ELEVATION_SMOOTHING_FACTOR); assertEquals(i, data.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR);
assertEquals(i, data.getTotalElevationGain(),
Constants.ELEVATION_SMOOTHING_FACTOR);
} }
} }
public void testGradeSimple() throws Exception { public void testGradeSimple() throws Exception {
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
// The value of the elevation does not matter. This is just to fill the // The value of the elevation does not matter. This is just to fill the
// buffer. // buffer.
builder.updateElevation(i); builder.updateElevation(i);
builder.updateGrade(100, 100); builder.updateGrade(100, 100);
if ((i > Constants.GRADE_SMOOTHING_FACTOR) if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) {
&& (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(1.0, builder.getTripStatistics().getMaxGrade());
assertEquals(1.0, builder.getStatistics().getMaxGrade()); assertEquals(1.0, builder.getTripStatistics().getMinGrade());
assertEquals(1.0, builder.getStatistics().getMinGrade());
} }
} }
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
// The value of the elevation does not matter. This is just to fill the // The value of the elevation does not matter. This is just to fill the
// buffer. // buffer.
builder.updateElevation(i); builder.updateElevation(i);
builder.updateGrade(100, -100); builder.updateGrade(100, -100);
if ((i > Constants.GRADE_SMOOTHING_FACTOR) if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) {
&& (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(1.0, builder.getTripStatistics().getMaxGrade());
assertEquals(1.0, builder.getStatistics().getMaxGrade()); assertEquals(-1.0, builder.getTripStatistics().getMinGrade());
assertEquals(-1.0, builder.getStatistics().getMinGrade());
} }
} }
} }
public void testGradeIgnoreShort() throws Exception { public void testGradeIgnoreShort() throws Exception {
for (double i = 0; i < 100; i++) { for (double i = 0; i < 100; i++) {
// The value of the elevation does not matter. This is just to fill the // The value of the elevation does not matter. This is just to fill the
// buffer. // buffer.
builder.updateElevation(i); builder.updateElevation(i);
builder.updateGrade(1, 100); builder.updateGrade(1, 100);
assertEquals(Double.NEGATIVE_INFINITY, builder.getStatistics().getMaxGrade()); assertEquals(Double.NEGATIVE_INFINITY, builder.getTripStatistics().getMaxGrade());
assertEquals(Double.POSITIVE_INFINITY, builder.getStatistics().getMinGrade()); assertEquals(Double.POSITIVE_INFINITY, builder.getTripStatistics().getMinGrade());
} }
} }
public void testUpdateSpeedIncludeZero() { public void testUpdateSpeedIncludeZero() {
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
builder.updateSpeed(i + 1000, 0.0, i, 4.0); builder.updateSpeed(i + 1000, 0.0, i, 4.0);
assertEquals(0.0, builder.getStatistics().getMaxSpeed()); assertEquals(0.0, builder.getTripStatistics().getMaxSpeed());
assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime());
} }
} }
public void testUpdateSpeedIngoreErrorCode() { public void testUpdateSpeedIngoreErrorCode() {
builder.updateSpeed(12345000, 128.0, 12344000, 0.0); builder.updateSpeed(12345000, 128.0, 12344000, 0.0);
assertEquals(0.0, builder.getStatistics().getMaxSpeed()); assertEquals(0.0, builder.getTripStatistics().getMaxSpeed());
assertEquals(1000, builder.getStatistics().getMovingTime());
} }
public void testUpdateSpeedIngoreLargeAcceleration() { public void testUpdateSpeedIngoreLargeAcceleration() {
builder.updateSpeed(12345000, 100.0, 12344000, 1.0); builder.updateSpeed(12345000, 100.0, 12344000, 1.0);
assertEquals(0.0, builder.getStatistics().getMaxSpeed()); assertEquals(0.0, builder.getTripStatistics().getMaxSpeed());
assertEquals(1000, builder.getStatistics().getMovingTime());
} }
public void testUpdateSpeed() { public void testUpdateSpeed() {
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
builder.updateSpeed(i + 1000, 4.0, i, 4.0); builder.updateSpeed(i + 1000, 4.0, i, 4.0);
assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime());
if (i > Constants.SPEED_SMOOTHING_FACTOR) { if (i > Constants.SPEED_SMOOTHING_FACTOR) {
assertEquals(4.0, builder.getStatistics().getMaxSpeed()); assertEquals(4.0, builder.getTripStatistics().getMaxSpeed());
} }
} }
} }