forked from upstream-mirrors/OpenTracks
59a05262a1
ImportAllTracks is essentially just a modification of ExportAllTracks that uses the existing GpxImporter to import all GPX files found in /sdcard/MyTracks/gpx. So that the import functionality can always be accessed, the MyTracks Activity's MenuManager is altered so that the Tracks button shows regardless of whether there are any tracks in the database (ImportAllTracks is launched by a button in the lower right of the MyTracksList activity). A Toast message shows if no GPX files are found; this serves as a hint to users on where to put GPX files for import so that this addition should be discoverable and useable without documentation. Im
363 lines
11 KiB
Java
363 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;
|
|
|
|
/**
|
|
* 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(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 given time.
|
|
*
|
|
* @param startTime the start time.
|
|
*/
|
|
public TripStatisticsBuilder(long startTime) {
|
|
data = 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.
|
|
*
|
|
* @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 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 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 don't want anyone messing with our internals
|
|
return new TripStatistics(data);
|
|
}
|
|
}
|