Files
OpenTracks/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java
T
2010-09-02 20:56:31 -03:00

372 lines
11 KiB
Java

/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.stats;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.location.Location;
import android.util.Log;
/**
* Statistics keeper for a trip.
*
* @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 = null;
/**
* The last location that contributed to the stats. It is also the last
* location the user was found to be moving.
*/
private Location lastMovingLocation = null;
/**
* 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(MyTracksConstants.SPEED_SMOOTHING_FACTOR);
/**
* A buffer of the recent elevation readings in meters.
*/
private final DoubleBuffer elevationBuffer =
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
/**
* A buffer of the distance between recent gps readings in meters.
*/
private final DoubleBuffer distanceBuffer =
new DoubleBuffer(MyTracksConstants.DISTANCE_SMOOTHING_FACTOR);
/**
* A buffer of the recent grade calculations.
*/
private final DoubleBuffer gradeBuffer =
new DoubleBuffer(MyTracksConstants.GRADE_SMOOTHING_FACTOR);
/**
* The total number of locations in this trip.
*/
private long totalLocations = 0;
/**
* Creates a new trip starting at the current system time.
*/
public TripStatisticsBuilder() {
data = new TripStatistics();
}
/**
* 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);
}
/**
* Adds a location to the current trip. This will update all of the internal
* variables with this new location.
*
* @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
*/
public boolean addLocation(Location currentLocation, long systemTime) {
if (paused) {
Log.w(MyTracksConstants.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 < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE
&& currentSpeed < MyTracksConstants.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.
*
* @param elevation the current elevation
*/
// @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;
}
/**
* 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(MyTracksConstants.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(MyTracksConstants.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 > MyTracksConstants.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 < MyTracksConstants.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;
}
currentGrade = elevationDifference / smoothedDistance;
gradeBuffer.setNext(currentGrade);
data.updateGradeExtremities(gradeBuffer.getAverage());
}
/**
* Pauses the track at the current time.
*/
public void pause() {
pauseAt(System.currentTimeMillis());
}
/**
* Pauses the track at the 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.
paused = true;
}
/**
* Resumes the current track at the current time.
*/
public void resume() {
resumeAt(System.currentTimeMillis());
}
/**
* Resumes the current track at the given time.
*
* @param time the time to resume at
*/
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
+ "}";
}
/**
* Returns the amount of time the user has been idle or 0 if they are moving.
*/
public long getIdleTime() {
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
*/
public double getSmoothedElevation() {
return elevationBuffer.getAverage();
}
public TripStatistics getStatistics() {
// Take a snapshot - we do't want anyone messing with our internals
return new TripStatistics(data);
}
}