forked from upstream-mirrors/OpenTracks
TrackDetailView: show connected bluetooth device.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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 androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
private int index;
|
||||
|
||||
// The sliding buffer of doubles.
|
||||
private final double[] buffer;
|
||||
|
||||
// True if the buffer is full
|
||||
private boolean isFull;
|
||||
|
||||
/**
|
||||
* 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 greater than 1.");
|
||||
}
|
||||
buffer = new double[size];
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buffer.
|
||||
*/
|
||||
public void reset() {
|
||||
index = 0;
|
||||
isFull = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
return sum / numberOfEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
double sumSquares = 0;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
sum += buffer[i];
|
||||
sumSquares += Math.pow(buffer[i], 2);
|
||||
}
|
||||
|
||||
double average = sum / numberOfEntries;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder("Full: ");
|
||||
builder.append(isFull);
|
||||
builder.append("\n");
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
builder.append((i == index) ? "<<" : "[");
|
||||
builder.append(buffer[i]);
|
||||
builder.append((i == index) ? ">> " : "] ");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
private double min;
|
||||
|
||||
// The largest value seen so far.
|
||||
private double max;
|
||||
|
||||
public ExtremityMonitor() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
boolean changed = false;
|
||||
if (value < min) {
|
||||
min = value;
|
||||
changed = true;
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum value.
|
||||
*
|
||||
* @param min the minimum value
|
||||
*/
|
||||
public void setMin(double min) {
|
||||
this.min = min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Min: " + min + " Max: " + max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
/*
|
||||
* Copyright 2010 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 android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Statistical data about a trip. The data in this class should be filled out by
|
||||
* TripStatisticsBuilder.
|
||||
* <p>
|
||||
* TODO: hashCode and equals
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TripStatistics implements Parcelable {
|
||||
|
||||
// The trip start time. This is the system time, might not match the GPs time.
|
||||
private long startTime = -1L;
|
||||
|
||||
// The trip stop time. This is the system time, might not match the GPS time.
|
||||
private long stopTime = -1L;
|
||||
|
||||
// The total trip distance (meters).
|
||||
private double totalDistance;
|
||||
|
||||
// The total time (ms). Updated when new points are received, may be stale.
|
||||
private long totalTime;
|
||||
|
||||
// The total moving time (ms). Based on when we believe the user is traveling.
|
||||
private long movingTime;
|
||||
|
||||
// The min and max latitude seen in this trip.
|
||||
private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
|
||||
|
||||
// The min and max longitude seen in this trip.
|
||||
private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor();
|
||||
|
||||
// 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 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() {}
|
||||
|
||||
/**
|
||||
* Copy constructor.
|
||||
*
|
||||
* @param other another statistics data object to copy from
|
||||
*/
|
||||
public TripStatistics(TripStatistics other) {
|
||||
startTime = other.startTime;
|
||||
stopTime = other.stopTime;
|
||||
totalDistance = other.totalDistance;
|
||||
totalTime = other.totalTime;
|
||||
movingTime = other.movingTime;
|
||||
latitudeExtremities.set(other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax());
|
||||
longitudeExtremities.set(other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax());
|
||||
maxSpeed = other.maxSpeed;
|
||||
elevationExtremities.set(other.elevationExtremities.getMin(), other.elevationExtremities.getMax());
|
||||
totalElevationGain = other.totalElevationGain;
|
||||
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 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;
|
||||
if (other.latitudeExtremities.hasData()) {
|
||||
latitudeExtremities.update(other.latitudeExtremities.getMin());
|
||||
latitudeExtremities.update(other.latitudeExtremities.getMax());
|
||||
}
|
||||
if (other.longitudeExtremities.hasData()) {
|
||||
longitudeExtremities.update(other.longitudeExtremities.getMin());
|
||||
longitudeExtremities.update(other.longitudeExtremities.getMax());
|
||||
}
|
||||
maxSpeed = Math.max(maxSpeed, other.maxSpeed);
|
||||
if (other.elevationExtremities.hasData()) {
|
||||
elevationExtremities.update(other.elevationExtremities.getMin());
|
||||
elevationExtremities.update(other.elevationExtremities.getMax());
|
||||
}
|
||||
totalElevationGain += other.totalElevationGain;
|
||||
if (other.gradeExtremities.hasData()) {
|
||||
gradeExtremities.update(other.gradeExtremities.getMin());
|
||||
gradeExtremities.update(other.gradeExtremities.getMax());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trip start time. The number of milliseconds since epoch.
|
||||
*/
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the trip start time.
|
||||
*
|
||||
* @param startTime the trip start time in milliseconds since the epoch
|
||||
*/
|
||||
public void setStartTime(long startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trip stop time. The number of milliseconds since epoch.
|
||||
*/
|
||||
public long getStopTime() {
|
||||
return stopTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the trip stop time.
|
||||
*
|
||||
* @param stopTime the stop time in milliseconds since the epoch
|
||||
*/
|
||||
public void setStopTime(long stopTime) {
|
||||
this.stopTime = stopTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total distance the user traveled in meters.
|
||||
*/
|
||||
public double getTotalDistance() {
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total trip distance.
|
||||
*
|
||||
* @param totalDistance the trip distance in meters
|
||||
*/
|
||||
public void setTotalDistance(double totalDistance) {
|
||||
this.totalDistance = totalDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to the current total distance.
|
||||
*
|
||||
* @param distance the distance to add in meters
|
||||
*/
|
||||
public void addTotalDistance(double distance) {
|
||||
totalDistance += distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the trip total time.
|
||||
*
|
||||
* @param totalTime the trip total time in milliseconds
|
||||
*/
|
||||
public void setTotalTime(long totalTime) {
|
||||
this.totalTime = totalTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moving time in milliseconds.
|
||||
*/
|
||||
public long getMovingTime() {
|
||||
return movingTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void addMovingTime(long time) {
|
||||
movingTime += time;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void updateLatitudeExtremities(double latitude) {
|
||||
latitudeExtremities.update(latitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a new longitude value.
|
||||
*
|
||||
* @param longitude the longitude value in signed decimal degrees
|
||||
*/
|
||||
public void updateLongitudeExtremities(double longitude) {
|
||||
longitudeExtremities.update(longitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return 0.0;
|
||||
}
|
||||
return totalDistance / ((double) totalTime / 1000.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average moving speed in meters/second.
|
||||
*/
|
||||
public double getAverageMovingSpeed() {
|
||||
if (movingTime == 0L) {
|
||||
return 0.0;
|
||||
}
|
||||
return totalDistance / ((double) movingTime / 1000.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum speed in meters/second.
|
||||
*/
|
||||
public double getMaxSpeed() {
|
||||
return Math.max(maxSpeed, getAverageMovingSpeed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum speed.
|
||||
*
|
||||
* @param maxSpeed the maximum speed in meters/second
|
||||
*/
|
||||
public void setMaxSpeed(double maxSpeed) {
|
||||
this.maxSpeed = maxSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum elevation.
|
||||
*
|
||||
* @param elevation the minimum elevation in meters
|
||||
*/
|
||||
public void setMinElevation(double elevation) {
|
||||
elevationExtremities.setMin(elevation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void updateElevationExtremities(double elevation) {
|
||||
elevationExtremities.update(elevation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void addTotalElevationGain(double gain) {
|
||||
totalElevationGain += gain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum grade for this trip.
|
||||
*/
|
||||
public double getMinGrade() {
|
||||
return gradeExtremities.getMin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum grade for this trip.
|
||||
*/
|
||||
public double getMaxGrade() {
|
||||
return gradeExtremities.getMax();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a new grade value.
|
||||
*
|
||||
* @param grade the grade value as a fraction
|
||||
*/
|
||||
public void updateGradeExtremities(double grade) {
|
||||
gradeExtremities.update(grade);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime()
|
||||
+ "; Total Distance: " + getTotalDistance() + "; Total Time: " + getTotalTime()
|
||||
+ "; Moving Time: " + getMovingTime() + "; Min Latitude: " + getBottomDegrees()
|
||||
+ "; Max Latitude: " + getTopDegrees() + "; Min Longitude: " + getLeftDegrees()
|
||||
+ "; Max Longitude: " + getRightDegrees() + "; Max Speed: " + getMaxSpeed()
|
||||
+ "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation()
|
||||
+ "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade()
|
||||
+ "; Max Grade: " + getMaxGrade() + "}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creator of statistics data from parcels.
|
||||
*/
|
||||
public static class Creator implements Parcelable.Creator<TripStatistics> {
|
||||
|
||||
@Override
|
||||
public TripStatistics createFromParcel(Parcel source) {
|
||||
TripStatistics data = new TripStatistics();
|
||||
|
||||
data.startTime = source.readLong();
|
||||
data.stopTime = source.readLong();
|
||||
data.totalDistance = source.readDouble();
|
||||
data.totalTime = source.readLong();
|
||||
data.movingTime = source.readLong();
|
||||
|
||||
double minLat = source.readDouble();
|
||||
double maxLat = source.readDouble();
|
||||
data.latitudeExtremities.set(minLat, maxLat);
|
||||
|
||||
double minLong = source.readDouble();
|
||||
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();
|
||||
data.gradeExtremities.set(minGrade, maxGrade);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TripStatistics[] newArray(int size) {
|
||||
return new TripStatistics[size];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creator of {@link TripStatistics} from parcels.
|
||||
*/
|
||||
public static final Creator CREATOR = new Creator();
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(startTime);
|
||||
dest.writeLong(stopTime);
|
||||
dest.writeDouble(totalDistance);
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* 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 android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
|
||||
import static com.google.android.apps.mytracks.services.TrackRecordingService.MAX_NO_MOVEMENT_SPEED;
|
||||
import static com.google.android.apps.mytracks.services.TrackRecordingService.PAUSE_LATITUDE;
|
||||
|
||||
/**
|
||||
* Updater for {@link TripStatistics}. For updating track trip statistics as new
|
||||
* locations are added. Note that some of the locations represent pause/resume
|
||||
* separator.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TripStatisticsUpdater {
|
||||
|
||||
private static final String TAG = TripStatisticsUpdater.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The number of elevation readings to smooth to get a somewhat accurate
|
||||
* signal.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final int ELEVATION_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* The number of run readings to smooth for calculating grade.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final int RUN_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* The number of grade readings to smooth to get a somewhat accurate signal.
|
||||
*/
|
||||
public static final int GRADE_SMOOTHING_FACTOR = 5;
|
||||
|
||||
/**
|
||||
* The number of speed reading to smooth to get a somewhat accurate signal.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final int SPEED_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* Ignore any acceleration faster than this. Will ignore any speeds that imply
|
||||
* acceleration greater than 2g's 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02
|
||||
* m/(m*ms)
|
||||
*/
|
||||
private static final double MAX_ACCELERATION = 0.02;
|
||||
|
||||
// The track's trip statistics
|
||||
private final TripStatistics tripStatistics;
|
||||
|
||||
// The current segment's trip statistics
|
||||
private TripStatistics currentSegment;
|
||||
|
||||
// Current segment's last location.
|
||||
private Location lastLocation;
|
||||
|
||||
// Current segment's last moving location
|
||||
private Location lastMovingLocation;
|
||||
|
||||
// A buffer of the recent elevation readings (m)
|
||||
private final DoubleBuffer elevationBuffer = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
// A buffer of the recent run readings (m) for calculating grade
|
||||
private final DoubleBuffer runBuffer = new DoubleBuffer(RUN_SMOOTHING_FACTOR);
|
||||
|
||||
// A buffer of the recent grade calculations (%)
|
||||
private final DoubleBuffer gradeBuffer = new DoubleBuffer(GRADE_SMOOTHING_FACTOR);
|
||||
|
||||
// A buffer of the recent speed readings (m/s) for calculating max speed
|
||||
private final DoubleBuffer speedBuffer = new DoubleBuffer(SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* Creates a new trip statistics updater.
|
||||
*
|
||||
* @param startTime the start time
|
||||
*/
|
||||
public TripStatisticsUpdater(long startTime) {
|
||||
tripStatistics = init(startTime);
|
||||
currentSegment = init(startTime);
|
||||
}
|
||||
|
||||
public void updateTime(long time) {
|
||||
currentSegment.setStopTime(time);
|
||||
currentSegment.setTotalTime(time - currentSegment.getStartTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track's trip statistics.
|
||||
*/
|
||||
public TripStatistics getTripStatistics() {
|
||||
// Take a snapshot - we don't want anyone messing with our tripStatistics
|
||||
TripStatistics stats = new TripStatistics(tripStatistics);
|
||||
stats.merge(currentSegment);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a location. TODO: This assume location has a valid time.
|
||||
*
|
||||
* @param location the location
|
||||
* @param minRecordingDistance the min recording distance
|
||||
*/
|
||||
public void addLocation(Location location, int minRecordingDistance) {
|
||||
// Always update time
|
||||
updateTime(location.getTime());
|
||||
if (!LocationUtils.isValidLocation(location)) {
|
||||
// Either pause or resume marker
|
||||
if (location.getLatitude() == PAUSE_LATITUDE) {
|
||||
if (lastLocation != null && lastMovingLocation != null
|
||||
&& lastLocation != lastMovingLocation) {
|
||||
currentSegment.addTotalDistance(lastMovingLocation.distanceTo(lastLocation));
|
||||
}
|
||||
tripStatistics.merge(currentSegment);
|
||||
}
|
||||
currentSegment = init(location.getTime());
|
||||
lastLocation = null;
|
||||
lastMovingLocation = null;
|
||||
elevationBuffer.reset();
|
||||
runBuffer.reset();
|
||||
gradeBuffer.reset();
|
||||
speedBuffer.reset();
|
||||
return;
|
||||
}
|
||||
currentSegment.updateLatitudeExtremities(location.getLatitude());
|
||||
currentSegment.updateLongitudeExtremities(location.getLongitude());
|
||||
|
||||
double elevationDifference = location.hasAltitude() ? updateElevation(location.getAltitude())
|
||||
: 0.0;
|
||||
|
||||
if (lastLocation == null || lastMovingLocation == null) {
|
||||
lastLocation = location;
|
||||
lastMovingLocation = location;
|
||||
return;
|
||||
}
|
||||
|
||||
double movingDistance = lastMovingLocation.distanceTo(location);
|
||||
if (movingDistance < minRecordingDistance
|
||||
&& (!location.hasSpeed() || location.getSpeed() < MAX_NO_MOVEMENT_SPEED)) {
|
||||
speedBuffer.reset();
|
||||
lastLocation = location;
|
||||
return;
|
||||
}
|
||||
long movingTime = location.getTime() - lastLocation.getTime();
|
||||
if (movingTime < 0) {
|
||||
lastLocation = location;
|
||||
return;
|
||||
}
|
||||
|
||||
// Update total distance
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
|
||||
// Update moving time
|
||||
currentSegment.addMovingTime(movingTime);
|
||||
|
||||
// Update grade
|
||||
double run = lastLocation.distanceTo(location);
|
||||
updateGrade(run, elevationDifference);
|
||||
|
||||
// Update max speed
|
||||
if (location.hasSpeed() && lastLocation.hasSpeed()) {
|
||||
updateSpeed(
|
||||
location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
|
||||
}
|
||||
|
||||
lastLocation = location;
|
||||
lastMovingLocation = location;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 double getSmoothedSpeed() {
|
||||
return speedBuffer.getAverage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (speed < MAX_NO_MOVEMENT_SPEED) {
|
||||
speedBuffer.reset();
|
||||
} else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) {
|
||||
speedBuffer.setNext(speed);
|
||||
if (speedBuffer.getAverage() > currentSegment.getMaxSpeed()) {
|
||||
currentSegment.setMaxSpeed(speedBuffer.getAverage());
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an elevation reading. Returns the difference.
|
||||
*
|
||||
* @param elevation the elevation
|
||||
*/
|
||||
@VisibleForTesting
|
||||
double updateElevation(double elevation) {
|
||||
// Update elevation using the smoothed average
|
||||
double oldAverage = elevationBuffer.getAverage();
|
||||
elevationBuffer.setNext(elevation);
|
||||
double newAverage = elevationBuffer.getAverage();
|
||||
|
||||
currentSegment.updateElevationExtremities(newAverage);
|
||||
double difference = newAverage - oldAverage;
|
||||
if (difference > 0) {
|
||||
currentSegment.addTotalElevationGain(difference);
|
||||
}
|
||||
return difference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a grade reading.
|
||||
*
|
||||
* @param run the run
|
||||
* @param rise the rise
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void updateGrade(double run, double rise) {
|
||||
runBuffer.setNext(run);
|
||||
|
||||
double smoothedRun = runBuffer.getAverage();
|
||||
|
||||
/*
|
||||
* With the error in the altitude measurement it is dangerous to divide by
|
||||
* anything less than 5.
|
||||
*/
|
||||
if (smoothedRun < 5.0) {
|
||||
return;
|
||||
}
|
||||
gradeBuffer.setNext(rise / smoothedRun);
|
||||
currentSegment.updateGradeExtremities(gradeBuffer.getAverage());
|
||||
}
|
||||
|
||||
private TripStatistics init(long time) {
|
||||
TripStatistics stats = new TripStatistics();
|
||||
stats.setStartTime(time);
|
||||
stats.setStopTime(time);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private boolean isValidSpeed(
|
||||
long time, double speed, long lastLocationTime, double lastLocationSpeed) {
|
||||
|
||||
/*
|
||||
* 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 > 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()) {
|
||||
double average = speedBuffer.getAverage();
|
||||
double diff = Math.abs(average - speed);
|
||||
return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user