From 40e2aba739ec907c3e63acf8090f6522e22edf53 Mon Sep 17 00:00:00 2001 From: Jimmy Shih Date: Wed, 8 Aug 2012 10:17:32 -0700 Subject: [PATCH] Clean up classes in the stats package - TripStatisticsBuilder - TripStatistics - ExtremityMonitor - DoubleBuffer --- .../apps/mytracks/io/file/GpxImporter.java | 2 +- .../SendFusionTablesAsyncTask.java | 4 +- .../mytracks/io/maps/SendMapsAsyncTask.java | 4 +- .../services/TrackRecordingService.java | 18 +- .../apps/mytracks/stats/DoubleBuffer.java | 92 ++- .../mytracks/stats/TripStatisticsBuilder.java | 517 +++++++------- .../apps/mytracks/stats/ExtremityMonitor.java | 79 +-- .../apps/mytracks/stats/TripStatistics.java | 653 ++++++++---------- .../stats/TripStatisticsBuilderTest.java | 119 ++-- 9 files changed, 680 insertions(+), 808 deletions(-) diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java index 06f155df2..1eb63e69a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java @@ -306,7 +306,7 @@ public class GpxImporter extends DefaultHandler { tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder.pauseAt(0); } - track.setTripStatistics(tripStatisticsBuilder.getStatistics()); + track.setTripStatistics(tripStatisticsBuilder.getTripStatistics()); track.setNumberOfPoints(numberOfLocations); myTracksProviderUtils.updateTrack(track); tracksIds.add(track.getId()); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java index 72810f2f0..31370e64a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -253,7 +253,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); if (i % elevationSamplingFrequency == 0) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); } lastLocation = location; @@ -279,7 +279,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { // Create an end marker if (lastLocation != null) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context); track.setDescription( diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java index 660e0ead4..e46720b8c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java @@ -297,7 +297,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask { elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); if (i % elevationSamplingFrequency == 0) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); } lastLocation = location; @@ -323,7 +323,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask { // Create an end marker if (lastLocation != null) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); track.setDescription(getTrackDescription(track, distances, elevations)); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index e50bac9f8..a11a2a969 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -654,7 +654,7 @@ public class TrackRecordingService extends Service { } } while (cursor.moveToPrevious()); } - statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); + statsBuilder.getTripStatistics().setMovingTime(stats.getMovingTime()); statsBuilder.pauseAt(stats.getStopTime()); statsBuilder.resumeAt(System.currentTimeMillis()); } else { @@ -834,7 +834,7 @@ public class TrackRecordingService extends Service { // Update the current track: if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90) { - TripStatistics tripStatistics = statsBuilder.getStatistics(); + TripStatistics tripStatistics = statsBuilder.getTripStatistics(); tripStatistics.setStopTime(System.currentTimeMillis()); if (recordingTrack.getStartId() < 0) { @@ -865,8 +865,8 @@ public class TrackRecordingService extends Service { if (waypoint != null) { waypoint.setLength(length); waypoint.setDuration( - System.currentTimeMillis() - statsBuilder.getStatistics().getStartTime()); - waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); + System.currentTimeMillis() - statsBuilder.getTripStatistics().getStartTime()); + waypoint.setTripStatistics(waypointStatsBuilder.getTripStatistics()); providerUtils.updateWaypoint(waypoint); } } @@ -896,7 +896,7 @@ public class TrackRecordingService extends Service { } waypoint.setTrackId(recordingTrackId); waypoint.setLength(length); - if (lastLocation == null || statsBuilder == null || statsBuilder.getStatistics() == null) { + if (lastLocation == null || statsBuilder == null || statsBuilder.getTripStatistics() == null) { if (!request.isTrackStatistics()) { return -1L; } @@ -910,7 +910,7 @@ public class TrackRecordingService extends Service { waypoint.setLocation(location); } else { waypoint.setLocation(lastLocation); - waypoint.setDuration(lastLocation.getTime() - statsBuilder.getStatistics().getStartTime()); + waypoint.setDuration(lastLocation.getTime() - statsBuilder.getTripStatistics().getStartTime()); } Uri uri = providerUtils.insertWaypoint(waypoint); 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 // 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 waypoint.setType(Waypoint.TYPE_STATISTICS); @@ -973,7 +973,7 @@ public class TrackRecordingService extends Service { name = getString(R.string.marker_split_name_format, nextMarkerNumber); } waypoint.setName(name); - waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); + waypoint.setTripStatistics(waypointStatsBuilder.getTripStatistics()); waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint)); waypoint.setIcon(getString(R.string.marker_statistics_icon_url)); @@ -1050,7 +1050,7 @@ public class TrackRecordingService extends Service { } public TripStatistics getTripStatistics() { - return statsBuilder.getStatistics(); + return statsBuilder.getTripStatistics(); } Location getLastLocation() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java b/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java index 356664617..7c86f0ba8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java @@ -20,66 +20,35 @@ package com.google.android.apps.mytracks.stats; * This class maintains a buffer of doubles. This buffer is a convenient class * for storing a series of doubles and calculating information about them. This * is a FIFO buffer. - * + * * @author Sandor Dornbush */ public class DoubleBuffer { - /** - * The location that the next write will occur at. - */ + // The location that the next write will occur at. private int index; - /** - * The sliding buffer of doubles. - */ + // The sliding buffer of doubles. private final double[] buffer; - /** - * Have all of the slots in the buffer been filled? - */ + // True if the buffer is full private boolean isFull; /** - * Creates a buffer with size elements. - * - * @param size the number of elements in the buffer - * @throws IllegalArgumentException if the size is not a positive value + * Creates a buffer with a certain size. + * + * @param size the size */ public DoubleBuffer(int size) { 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]; reset(); } /** - * Adds a double to the buffer. If the buffer is full the oldest element is - * 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. + * Resets the buffer. */ public void reset() { index = 0; @@ -87,16 +56,20 @@ public class DoubleBuffer { } /** - * Gets the average of values from the buffer. - * - * @return The average of the buffer + * Returns true if the buffer is full. + */ + public boolean isFull() { + return isFull; + } + + /** + * Gets the average of the buffer. */ public double getAverage() { int numberOfEntries = isFull ? buffer.length : index; if (numberOfEntries == 0) { return 0; } - double sum = 0; for (int i = 0; i < numberOfEntries; i++) { sum += buffer[i]; @@ -105,15 +78,15 @@ public class DoubleBuffer { } /** - * Gets the average and standard deviation of the buffer. - * - * @return An array of two elements - the first is the average, and the second - * is the variance + * Gets the average and the variance of the buffer. + * + * @return an array of two elements - the first is the average, the second is + * the variance */ public double[] getAverageAndVariance() { int numberOfEntries = isFull ? buffer.length : index; if (numberOfEntries == 0) { - return new double[]{0, 0}; + return new double[] { 0, 0 }; } double sum = 0; @@ -124,8 +97,25 @@ public class DoubleBuffer { } double average = sum / numberOfEntries; - return new double[]{average, - sumSquares / numberOfEntries - Math.pow(average, 2)}; + double variance = 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 diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index 3f6f7e770..18f7103b9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -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.util.PreferencesUtils; +import com.google.common.annotations.VisibleForTesting; import android.location.Location; 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 Rodrigo Damazio */ public class TripStatisticsBuilder { - /** - * Statistical data about the trip, which can be displayed to the user. - */ - private final TripStatistics data; - /** - * The last location that the gps reported. - */ - 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 trip statistics. + private final TripStatistics tripStatistics; + // The minimum recording distance. 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) { - data = new TripStatistics(); + tripStatistics = new TripStatistics(); resumeAt(startTime); } /** - * Creates a new trip, starting with existing statistics data. - * - * @param statsData the statistics data to copy and start from - */ - public TripStatisticsBuilder(TripStatistics statsData) { - data = new TripStatistics(statsData); - if (data.getStartTime() > 0) { - resumeAt(data.getStartTime()); - } - } - - /** - * Adds a location to the current trip. This will update all of the internal - * variables with this new location. + * Creates a new trip, starting with an existing {@link TripStatistics}. * - * @param currentLocation the current gps location - * @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 + * @param other the existing {@link TripStatistics} */ - public boolean addLocation(Location currentLocation, long systemTime) { - if (paused) { - Log.w(TAG, - "Tried to account for location while track is paused"); - return false; + public TripStatisticsBuilder(TripStatistics other) { + tripStatistics = new TripStatistics(other); + if (tripStatistics.getStartTime() > 0) { + resumeAt(tripStatistics.getStartTime()); } - - 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. - */ - private void updateBounds(Location location) { - data.updateLatitudeExtremities(location.getLatitude()); - data.updateLongitudeExtremities(location.getLongitude()); - } - - /** - * Updates the elevation measurements. + * Sets the min recording distance. * - * @param elevation the current elevation + * @param minRecordingDistance the min recording distance */ - // @VisibleForTesting - double updateElevation(double elevation) { - 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; + public void setMinRecordingDistance(int minRecordingDistance) { + this.minRecordingDistance = minRecordingDistance; } /** - * Updates the speed measurements. + * Resumes the current track at a given time. * - * @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 + * @param time the time */ - // @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) { + public void resumeAt(long time) { + if (!paused) { return; } - currentGrade = elevationDifference / smoothedDistance; - gradeBuffer.setNext(currentGrade); - data.updateGradeExtremities(gradeBuffer.getAverage()); + + tripStatistics.setStartTime(time); + 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 */ public void pauseAt(long time) { - if (paused) { return; } - - data.setStopTime(time); - data.setTotalTime(time - data.getStartTime()); - lastLocation = null; // Make sure the counter restarts. + if (paused) { + return; + } + tripStatistics.setStopTime(time); + // TODO: total time needs to take into account pauses + tripStatistics.setTotalTime(time - tripStatistics.getStartTime()); paused = true; } /** - * Resumes the current track at the given time. - * - * @param time the time to resume at + * Gets the trip statistics. */ - public void resumeAt(long time) { - if (!paused) { return; } - - // 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 - + "}"; + public TripStatistics getTripStatistics() { + // Take a snapshot - we don't want anyone messing with our internals + return new TripStatistics(tripStatistics); } /** - * 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() { - if (lastLocation == null || lastMovingLocation == null) + if (lastLocation == null || lastMovingLocation == null) { return 0; + } return lastLocation.getTime() - lastMovingLocation.getTime(); } /** - * Gets the current elevation smoothed over several readings. The elevation - * data is very noisy so it is better to use the smoothed elevation than the - * raw elevation for many tasks. - * - * @return The elevation smoothed over several readings + * Gets the smoothed elevation over several readings. The elevation readings + * is noisy so the smoothed elevation is better than the raw elevation for + * many tasks. */ public double getSmoothedElevation() { return elevationBuffer.getAverage(); } - public TripStatistics getStatistics() { - // Take a snapshot - we don't want anyone messing with our internals - return new TripStatistics(data); + /** + * Adds a location. This will update all of the internal variables with this + * 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); } } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java index bb65ee76d..8a4618588 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java @@ -18,19 +18,15 @@ package com.google.android.apps.mytracks.stats; /** * A helper class that tracks a minimum and a maximum of a variable. - * + * * @author Sandor Dornbush */ public class ExtremityMonitor { - /** - * The smallest value seen so far. - */ + // The smallest value seen so far. private double min; - /** - * The largest value seen so far. - */ + // The largest value seen so far. private double max; public ExtremityMonitor() { @@ -38,9 +34,31 @@ public class ExtremityMonitor { } /** - * Updates the min and the max with the new value. - * - * @param value the new value for the monitor + * 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 * @return true if an extremity was found */ public boolean update(double value) { @@ -56,34 +74,11 @@ public class ExtremityMonitor { 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. + * + * @param min the minimum value + * @param max the maximum value */ public void set(double min, double max) { this.min = min; @@ -92,6 +87,8 @@ public class ExtremityMonitor { /** * Sets the minimum value. + * + * @param min the minimum value */ public void setMin(double min) { this.min = min; @@ -99,16 +96,20 @@ public class ExtremityMonitor { /** * Sets the maximum value. + * + * @param max the maximum value */ public void setMax(double max) { this.max = max; } + /** + * Returns true if has data. + */ public boolean hasData() { - return min != Double.POSITIVE_INFINITY - && max != Double.NEGATIVE_INFINITY; + return min != Double.POSITIVE_INFINITY && max != Double.NEGATIVE_INFINITY; } - + @Override public String toString() { return "Min: " + min + " Max: " + max; diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 4d82d2814..892c0d590 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -13,182 +13,226 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.stats; import android.os.Parcel; import android.os.Parcelable; /** - * Statistical data about a trip. - * The data in this class should be filled out by TripStatisticsBuilder. - * + * Statistical data about a trip. The data in this class should be filled out by + * TripStatisticsBuilder. + *

* TODO: hashCode and equals - * + * * @author Rodrigo Damazio */ public class TripStatistics implements Parcelable { - /** - * The start time for the trip. This is system time which might not match gps - * time. - */ + // The trip start time. This is the system time, might not match the GPs time. private long startTime = -1L; - /** - * The stop time for the trip. This is the system time which might not match - * gps time. - */ + // The trip stop time. This is the system time, might not match the GPS time. private long stopTime = -1L; - /** - * 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. - */ + // The total trip distance (meters). private double totalDistance; - /** - * The total elevation gained on this trip in meters. - */ - private double totalElevationGain; + // The total time (ms). Updated when new points are received, may be stale. + private long totalTime; - /** - * The maximum speed in meters/second reported that we believe to be a valid - * speed. - */ - private double maxSpeed; + // The total moving time (ms). Based on when we believe the user is traveling. + private long movingTime; - /** - * The min and max latitude values seen in this trip. - */ + // The min and max latitude seen in this trip. private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); - /** - * The min and max longitude values seen in this trip. - */ + // The min and max longitude seen in this trip. private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor(); - /** - * The min and max elevation seen on this trip in meters. - */ + // The maximum speed (meters/second) that we believe is valid. + private double maxSpeed; + + // The min and max elevation (meters) seen on this trip. private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); - /** - * The minimum and maximum grade calculations on this trip. - */ + // The total elevation gained (meters). + private double totalElevationGain; + + // The min and max grade seen on this trip. private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); /** * Default constructor. */ - public TripStatistics() { - } + public TripStatistics() {} /** * Copy constructor. - * + * * @param other another statistics data object to copy from */ public TripStatistics(TripStatistics other) { - this.maxSpeed = other.maxSpeed; - this.movingTime = other.movingTime; this.startTime = other.startTime; this.stopTime = other.stopTime; this.totalDistance = other.totalDistance; - this.totalElevationGain = other.totalElevationGain; this.totalTime = other.totalTime; - - this.latitudeExtremities.set(other.latitudeExtremities.getMin(), - other.latitudeExtremities.getMax()); - this.longitudeExtremities.set(other.longitudeExtremities.getMin(), - other.longitudeExtremities.getMax()); - this.elevationExtremities.set(other.elevationExtremities.getMin(), - other.elevationExtremities.getMax()); - this.gradeExtremities.set(other.gradeExtremities.getMin(), - other.gradeExtremities.getMax()); + this.movingTime = other.movingTime; + this.latitudeExtremities.set( + other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax()); + this.longitudeExtremities.set( + other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax()); + this.maxSpeed = other.maxSpeed; + this.elevationExtremities.set( + other.elevationExtremities.getMin(), other.elevationExtremities.getMax()); + this.totalElevationGain = other.totalElevationGain; + this.gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax()); } /** - * Combines these statistics with those from another object. - * This assumes that the time periods covered by each do not intersect. - * - * @param other the other waypoint + * Combines these statistics with those from another object. This assumes that + * the time periods covered by each do not intersect. + * + * @param other another statistics data object */ public void merge(TripStatistics other) { startTime = Math.min(startTime, other.startTime); stopTime = Math.max(stopTime, other.stopTime); + totalDistance += other.totalDistance; totalTime += other.totalTime; 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()); - longitudeExtremities.update(other.longitudeExtremities.getMax()); + latitudeExtremities.update(other.latitudeExtremities.getMax()); 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()); - gradeExtremities.update(other.gradeExtremities.getMax()); + elevationExtremities.update(other.elevationExtremities.getMax()); + totalElevationGain += other.totalElevationGain; gradeExtremities.update(other.gradeExtremities.getMin()); + gradeExtremities.update(other.gradeExtremities.getMax()); } /** - * Gets the time that this track started. - * - * @return The number of milliseconds since epoch to the time when this track - * started + * Gets the trip start time. The number of milliseconds since epoch. */ public long getStartTime() { return startTime; } /** - * Gets the time that this track stopped. - * - * @return The number of milliseconds since epoch to the time when this track - * stopped + * Gets the trip stop time. The number of milliseconds since epoch. */ public long getStopTime() { return stopTime; } /** - * Gets the total time that this track has been active. - * 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 + * Gets the total distance the user traveled in meters. */ public double getTotalDistance() { return totalDistance; } /** - * Gets the the average speed the user traveled. - * This calculation only takes into account the displacement until the last - * point that was accounted for in statistics. - * - * @return The average speed in m/s + * Gets the total time in milliseconds that this track has been active. 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. + */ + 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() { if (totalTime == 0L) { @@ -198,10 +242,7 @@ public class TripStatistics implements Parcelable { } /** - * Gets the the average speed the user traveled when they were actively - * moving. - * - * @return The average moving speed in m/s + * Gets the average moving speed in meters/second. */ public double getAverageMovingSpeed() { if (movingTime == 0L) { @@ -211,261 +252,80 @@ public class TripStatistics implements Parcelable { } /** - * Gets the the maximum speed for this track. - * - * @return The maximum speed in m/s + * Gets the maximum speed in meters/second. */ public double getMaxSpeed() { return maxSpeed; } /** - * Gets the moving time. - * - * @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 + * Gets the minimum elevation. This is calculated from the smoothed elevation + * so this can actually be more than the current elevation. */ public double getMinElevation() { return elevationExtremities.getMin(); } /** - * Gets the maximum elevation seen on this trip. This is calculated from the - * smoothed elevation so this can actually be less than the current elevation. - * - * @return The largest elevation reading for this trip in meters + * Gets the maximum elevation. This is calculated from the smoothed elevation + * so this can actually be less than the current elevation. */ public double getMaxElevation() { 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. - * - * @return The maximum grade for this trip as a fraction */ public double getMaxGrade() { return gradeExtremities.getMax(); } /** - * Gets the minimum grade for this trip. - * - * @return The minimum grade for this trip as a fraction - */ - 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 + * Sets the trip start time. + * + * @param startTime the trip start time in milliseconds since the epoch */ public void setStartTime(long startTime) { this.startTime = startTime; } /** - * Sets the stop time for this trip. - * - * @param stopTime the stop time, in milliseconds since the epoch + * Sets the trip stop time. + * + * @param stopTime the stop time in milliseconds since the epoch */ public void setStopTime(long 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. - * + * * @param totalDistance the trip distance in meters */ public void setTotalDistance(double 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. - * + * * @param distance the distance to add in meters */ void addTotalDistance(double distance) { @@ -473,17 +333,26 @@ public class TripStatistics implements Parcelable { } /** - * Adds to the total elevation variation. - * - * @param gain the elevation variation in meters + * Sets the trip total time. + * + * @param totalTime the trip total time in milliseconds */ - void addTotalElevationGain(double gain) { - totalElevationGain += gain; + public void setTotalTime(long totalTime) { + 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 */ void addMovingTime(long time) { @@ -491,8 +360,22 @@ 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 */ void updateLatitudeExtremities(double latitude) { @@ -500,8 +383,8 @@ 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 */ void updateLongitudeExtremities(double longitude) { @@ -509,8 +392,35 @@ 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 */ void updateElevationExtremities(double elevation) { @@ -518,49 +428,77 @@ 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 */ void updateGradeExtremities(double grade) { gradeExtremities.update(grade); } - // String conversion - @Override public String toString() { - return "TripStatistics { Start Time: " + getStartTime() - + "; Total Time: " + getTotalTime() - + "; Moving Time: " + getMovingTime() - + "; Total Distance: " + getTotalDistance() - + "; Elevation Gain: " + getTotalElevationGain() - + "; Min Elevation: " + getMinElevation() - + "; Max Elevation: " + getMaxElevation() - + "; Average Speed: " + getAverageMovingSpeed() - + "; Min Grade: " + getMinGrade() - + "; Max Grade: " + getMaxGrade() - + "}"; + return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime() + + "; Total Time: " + getTotalTime() + "; Total Distance: " + getTotalDistance() + + "; Total Time: " + getTotalTime() + "; Moving Time: " + getMovingTime() + + "; Min Latitude: " + getBottomDegrees() + "; Max Latitude: " + getTopDegrees() + + "; Min Longitude: " + getLeftDegrees() + "; Max Longitude: " + getRightDegrees() + + "; Max Elevation: " + getMaxElevation() + "; Max Speed: " + getMaxSpeed() + + "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation() + + "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade() + + "; Max Grade: " + getMaxGrade() + "}"; } - // Parcelable interface and creator - /** * Creator of statistics data from parcels. */ - public static class Creator - implements Parcelable.Creator { + public static class Creator implements Parcelable.Creator { @Override public TripStatistics createFromParcel(Parcel source) { TripStatistics data = new TripStatistics(); data.startTime = source.readLong(); - data.movingTime = source.readLong(); - data.totalTime = source.readLong(); + data.stopTime = source.readLong(); data.totalDistance = source.readDouble(); - data.totalElevationGain = source.readDouble(); - data.maxSpeed = source.readDouble(); + data.totalTime = source.readLong(); + data.movingTime = source.readLong(); double minLat = source.readDouble(); double maxLat = source.readDouble(); @@ -570,9 +508,12 @@ public class TripStatistics implements Parcelable { double maxLong = source.readDouble(); data.longitudeExtremities.set(minLong, maxLong); + data.maxSpeed = source.readDouble(); + double minElev = source.readDouble(); double maxElev = source.readDouble(); data.elevationExtremities.set(minElev, maxElev); + data.totalElevationGain = source.readDouble(); double minGrade = source.readDouble(); double maxGrade = source.readDouble(); @@ -600,18 +541,18 @@ public class TripStatistics implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(startTime); - dest.writeLong(movingTime); - dest.writeLong(totalTime); + dest.writeLong(stopTime); dest.writeDouble(totalDistance); - dest.writeDouble(totalElevationGain); - dest.writeDouble(maxSpeed); - + dest.writeLong(totalTime); + dest.writeLong(movingTime); dest.writeDouble(latitudeExtremities.getMin()); dest.writeDouble(latitudeExtremities.getMax()); dest.writeDouble(longitudeExtremities.getMin()); dest.writeDouble(longitudeExtremities.getMax()); + dest.writeDouble(maxSpeed); dest.writeDouble(elevationExtremities.getMin()); dest.writeDouble(elevationExtremities.getMax()); + dest.writeDouble(totalElevationGain); dest.writeDouble(gradeExtremities.getMin()); dest.writeDouble(gradeExtremities.getMax()); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java index 750bc31de..8b897e2e3 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java @@ -10,7 +10,7 @@ import junit.framework.TestCase; /** * Test the the function of the TripStatisticsBuilder class. - * + * * @author Sandor Dornbush */ public class TripStatisticsBuilderTest extends TestCase { @@ -25,55 +25,49 @@ public class TripStatisticsBuilderTest extends TestCase { public void testAddLocationSimple() throws Exception { builder = new TripStatisticsBuilder(1000); - TripStatistics stats = builder.getStatistics(); + TripStatistics tripStatistics = builder.getTripStatistics(); assertEquals(0.0, builder.getSmoothedElevation()); - assertEquals(Double.POSITIVE_INFINITY, stats.getMinElevation()); - assertEquals(Double.NEGATIVE_INFINITY, stats.getMaxElevation()); - assertEquals(0.0, stats.getMaxSpeed()); - assertEquals(Double.POSITIVE_INFINITY, stats.getMinGrade()); - assertEquals(Double.NEGATIVE_INFINITY, stats.getMaxGrade()); - assertEquals(0.0, stats.getTotalElevationGain()); - assertEquals(0, stats.getMovingTime()); - assertEquals(0.0, stats.getTotalDistance()); + assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinElevation()); + assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxElevation()); + assertEquals(0.0, tripStatistics.getMaxSpeed()); + assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinGrade()); + assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxGrade()); + assertEquals(0.0, tripStatistics.getTotalElevationGain()); + assertEquals(0, tripStatistics.getMovingTime()); + assertEquals(0.0, tripStatistics.getTotalDistance()); for (int i = 0; i < 100; i++) { - Location l = new Location("test"); - l.setAccuracy(1.0f); - l.setLongitude(45.0); + Location location = new Location("test"); + location.setAccuracy(1.0f); + location.setLongitude(45.0); - // Going up by 5 meters each time. - l.setAltitude(i); - // Moving by .1% of a degree latitude. - l.setLatitude(i * .001); - l.setSpeed(11.1f); + // Going up by 1 meter each time. + location.setAltitude(i); + location.setLatitude(i * .001); // Moving by .001 degree latitude (111 meters) + location.setSpeed(11.1f); // Each time slice is 10 seconds. long time = 1000 + 10000 * i; - l.setTime(time); - boolean moving = builder.addLocation(l, time); + location.setTime(time); + boolean moving = builder.addLocation(location, time); assertEquals((i != 0), moving); - stats = builder.getStatistics(); - assertEquals(10000 * i, stats.getTotalTime()); - assertEquals(10000 * i, stats.getMovingTime()); - assertEquals(i, builder.getSmoothedElevation(), - Constants.ELEVATION_SMOOTHING_FACTOR / 2); - assertEquals(0.0, stats.getMinElevation()); - assertEquals(i, stats.getMaxElevation(), - Constants.ELEVATION_SMOOTHING_FACTOR / 2); - assertEquals(i, stats.getTotalElevationGain(), - Constants.ELEVATION_SMOOTHING_FACTOR); + tripStatistics = builder.getTripStatistics(); + assertEquals(10000 * i, tripStatistics.getTotalTime()); + assertEquals(10000 * i, tripStatistics.getMovingTime()); + assertEquals(i, builder.getSmoothedElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2); + assertEquals(0.0, tripStatistics.getMinElevation()); + assertEquals(i, tripStatistics.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2); + assertEquals(i, tripStatistics.getTotalElevationGain(), Constants.ELEVATION_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) - && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { - assertEquals(0.009, stats.getMinGrade(), 0.0001); - assertEquals(0.009, stats.getMaxGrade(), 0.0001); + if (i > Constants.DISTANCE_SMOOTHING_FACTOR && i > Constants.ELEVATION_SMOOTHING_FACTOR) { + // 1 m / 111 m = .009 + assertEquals(0.009, tripStatistics.getMinGrade(), 0.0001); + assertEquals(0.009, tripStatistics.getMaxGrade(), 0.0001); } - // 1 degree = 111 km - // 1 timeslice = 0.001 degree = 111 m - assertEquals(111.0 * i, stats.getTotalDistance(), 100); + assertEquals(111.0 * i, tripStatistics.getTotalDistance(), 100); } } @@ -86,7 +80,7 @@ public class TripStatisticsBuilderTest extends TestCase { for (int j = 0; j < 100; j++) { assertEquals(0.0, builder.updateElevation(elevation)); assertEquals(elevation, builder.getSmoothedElevation()); - TripStatistics data = builder.getStatistics(); + TripStatistics data = builder.getTripStatistics(); assertEquals(elevation, data.getMinElevation()); assertEquals(elevation, data.getMaxElevation()); assertEquals(0.0, data.getTotalElevationGain()); @@ -104,80 +98,71 @@ public class TripStatisticsBuilderTest extends TestCase { } else { expectedGain = 1.0; } - assertEquals(expectedGain, - builder.updateElevation(i)); + assertEquals(expectedGain, builder.updateElevation(i)); assertEquals(i, builder.getSmoothedElevation(), 20); - TripStatistics data = builder.getStatistics(); + TripStatistics data = builder.getTripStatistics(); assertEquals(0.0, data.getMinElevation(), 0.0); - assertEquals(i, data.getMaxElevation(), - Constants.ELEVATION_SMOOTHING_FACTOR); - assertEquals(i, data.getTotalElevationGain(), - Constants.ELEVATION_SMOOTHING_FACTOR); + assertEquals(i, data.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR); + assertEquals(i, data.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR); } } public void testGradeSimple() throws Exception { 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. builder.updateElevation(i); builder.updateGrade(100, 100); - if ((i > Constants.GRADE_SMOOTHING_FACTOR) - && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { - assertEquals(1.0, builder.getStatistics().getMaxGrade()); - assertEquals(1.0, builder.getStatistics().getMinGrade()); + if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { + assertEquals(1.0, builder.getTripStatistics().getMaxGrade()); + assertEquals(1.0, builder.getTripStatistics().getMinGrade()); } } 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. builder.updateElevation(i); builder.updateGrade(100, -100); - if ((i > Constants.GRADE_SMOOTHING_FACTOR) - && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { - assertEquals(1.0, builder.getStatistics().getMaxGrade()); - assertEquals(-1.0, builder.getStatistics().getMinGrade()); + if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { + assertEquals(1.0, builder.getTripStatistics().getMaxGrade()); + assertEquals(-1.0, builder.getTripStatistics().getMinGrade()); } } } public void testGradeIgnoreShort() throws Exception { 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. builder.updateElevation(i); builder.updateGrade(1, 100); - assertEquals(Double.NEGATIVE_INFINITY, builder.getStatistics().getMaxGrade()); - assertEquals(Double.POSITIVE_INFINITY, builder.getStatistics().getMinGrade()); + assertEquals(Double.NEGATIVE_INFINITY, builder.getTripStatistics().getMaxGrade()); + assertEquals(Double.POSITIVE_INFINITY, builder.getTripStatistics().getMinGrade()); } } public void testUpdateSpeedIncludeZero() { for (int i = 0; i < 1000; i++) { builder.updateSpeed(i + 1000, 0.0, i, 4.0); - assertEquals(0.0, builder.getStatistics().getMaxSpeed()); - assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime()); + assertEquals(0.0, builder.getTripStatistics().getMaxSpeed()); } } public void testUpdateSpeedIngoreErrorCode() { builder.updateSpeed(12345000, 128.0, 12344000, 0.0); - assertEquals(0.0, builder.getStatistics().getMaxSpeed()); - assertEquals(1000, builder.getStatistics().getMovingTime()); + assertEquals(0.0, builder.getTripStatistics().getMaxSpeed()); } public void testUpdateSpeedIngoreLargeAcceleration() { builder.updateSpeed(12345000, 100.0, 12344000, 1.0); - assertEquals(0.0, builder.getStatistics().getMaxSpeed()); - assertEquals(1000, builder.getStatistics().getMovingTime()); + assertEquals(0.0, builder.getTripStatistics().getMaxSpeed()); } public void testUpdateSpeed() { for (int i = 0; i < 1000; i++) { builder.updateSpeed(i + 1000, 4.0, i, 4.0); - assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime()); if (i > Constants.SPEED_SMOOTHING_FACTOR) { - assertEquals(4.0, builder.getStatistics().getMaxSpeed()); + assertEquals(4.0, builder.getTripStatistics().getMaxSpeed()); } } }